为什么这个多线程代码会超出它的 while 条件?

Why does this multithreaded code overrun its while condition?

此代码旨在说明多线程代码如何踩到其共享变量

#python3.6
from concurrent.futures import ThreadPoolExecutor


def worker(counts, counter):
    counts.append(counter)

for i in range(10):
    counter = 0
    counts = []
    with ThreadPoolExecutor(max_workers=4) as executor:
        while len(counts) < 1000:
            executor.submit(worker, counts, counter)
            counter += 1
        print("counter = {} length counts = {} max(counts) = {}".
            format(counter, len(counts), max(counts)))

典型输出为:

counter = 1217 length counts = 1216 max(counts) = 1215
counter = 1209 length counts = 1185 max(counts) = 1184
counter = 1124 length counts = 1124 max(counts) = 1123
counter = 1339 length counts = 1338 max(counts) = 1337
counter = 1179 length counts = 1178 max(counts) = 1177
counter = 1032 length counts = 1002 max(counts) = 1001
counter = 1001 length counts = 1000 max(counts) = 999
counter = 1001 length counts = 1000 max(counts) = 999
counter = 1201 length counts = 1201 max(counts) = 1200
counter = 1306 length counts = 1304 max(counts) = 1304

我原以为长度和最大值会与 1000 有小的偏差,但 999 到 1500 之间的数字是正常的。

鉴于 while 块应该在 counts 达到 999 的长度时完成,并且 append 操作应该是线程安全的,为什么结果差异如此之大?我预计会有小错误,而不是像这些。

从任务提交给执行器到开始执行的延迟是一个具有某种概率分布的随机变量D。我们可以假设只有这个任务是 运行ning(没有后台任务等),并且每个代码行都需要一个时间片来执行。为简单起见,假设不存在对四个并发任务的限制。这些是非常简化的假设,可以使分析此行为至少在某种程度上易于处理。

假设D同样取值为0,即所有worker立即开始执行。这是同步情况。我想我们都可以同意,在这种情况下,你每次都会得到 999 的计数器,没有变化。

现在,假设 D 在 4 到 13(含)之间均匀分布。也就是说,任何任务的执行都有 10% 的几率被 4、5、6、7、8、9、10、11、12 或 13 个时间量中的任何一个延迟。假设我们要循环直到计数器等于 10。最好的情况是一切都延迟最少。 while len(counts) < 10 行将在 1, 4, 7, ..., 3k - 2 时刻执行,并在 2, 5, 8, ..., 3k - 1 时刻启动任务。第 10 个任务 运行 行 counts.append(counter) 在时间 33 = 3(10) - 1 + 4。接下来在时间 34 = 3(12)-2 检查循环条件,这意味着循环的 11 次迭代已经完成,第 12 次不会 运行。因此 counter = 11,并且我们安排了一个尚未 运行 的任务(但仍然会在某个时候 运行;在我们的例子中,在您打印结果之前,但通常有一个打印竞争条件)。

在最坏的情况下,延迟为 13,因此将 33 更改为 42,您会得到第 15 次失败的循环条件,这意味着 counter = 14 并且有4 个未完成的任务,在您格式化打印输出之前可能会或可能不会完成。

因此,在最好的情况下,列表长度为 11 到 12(取决于打印输出的竞争条件),在最坏的情况下,列表长度为 14 到 18 (取决于比赛条件)。我希望大量试验的结果大致呈正态分布,平均值约为 ~14.5,标准差约为 1.2。

无论目标如何,您都希望误差的幅度大致相同,因此通过使目标更大,您会看到相对较小的误差(尽管幅度相同)。这是因为误差的大小仅取决于随机变量的分布。这可能就是为什么您会看到它收敛到更高的值:您的任务调度程序具有相同的延迟分布,但随着您增加该期望,它与总体期望相比变得不那么相关。目标越低,相对来说效果越大。

根据你得到的数字 = 比如说,从 ~1000 到 ~1500 - 如果上述模型粗略计算,我猜你的线路操作指标延迟大约是 250-900 线路操作持有。当然,在 250-900 的范围内分布可能不均匀,但这可能是一个粗略的猜测。您可以使用计时器自己检查分布,并与循环的定时平均周期长度进行比较。此外,你有一个 max workers given 通过增加底层系统的有效延迟来改变分析(假设系统可以真正并行地执行任意多个进程)。

如您所见,即使采用所有简化假设,这也是一个模糊的分析,因此要真正确定为什么您在真实计算机上看到您的分布将是一项艰巨的任务。