【问题标题】:Why does this multithreaded code overrun its while condition?为什么这个多线程代码会超出它的 while 条件?
【发布时间】:2017-09-07 23:31:19
【问题描述】:

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

#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 操作应该是线程安全的,为什么结果会有这么大的差异?我预计会有小错误,而不是这些。

【问题讨论】:

  • 因为工人不会立即被执行。他们将在未来的某个时间运行,但你不知道什么时候。在检查循环条件之前,您不能指望它们被执行。相反,您的代码将继续启动工作程序,直到运行足够多的工作以将 1,000 个事物放入列表中。但是,已经提交了更多任务,但还没有机会运行。您可以将计数器的值添加到打印语句中吗?这会让您知道实际提交了多少任务。
  • 补充一点:不能保证这甚至会终止。如果线程池永远不会获得 CPU 份额,则可以永远运行循环(或者更确切地说,直到队列内存不足)。
  • 仅供参考,list.append() 可以是线程安全的。请参阅Python FAQ
  • 由于线程化的worker 函数有效地改变了counts 列表的长度,使用该列表的长度来控制while 循环体的执行并不是一个可靠的方法确定要进行多少次executor.submit() 调用——因为之前对其的调用可能已经执行,也可能尚未执行worker(并更新了列表的长度)。这可以解释为什么 counter 中的值比您明显预期的要高。
  • @jszakmeister "此代码旨在说明多线程代码如何踩踏其共享变量"?

标签: python multithreading theory


【解决方案1】:

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

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

现在,假设 D 从 4 到 13 均匀分布,包括 4 到 13。也就是说,任何任务的执行有 10% 的机会被 4、5、6、7、8、9、10、11、12 或 13 个时间量中的任何一个延迟。假设我们要循环直到计数器等于 10。最好的情况是一切都被最小地延迟。 while len(counts) &lt; 10 行有时会执行 1, 4, 7, ..., 3k - 2,它有时会启动任务 2, 5, 8, ..., 3k - 1。第 10 个任务在 33 = 3(10) - 1 + 4 时间运行 counts.append(counter) 行。接下来在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 范围内分布可能并不均匀,但这可能是一个粗略的猜测。您可以使用计时器自己检查分布,并与循环的定时平均周期长度进行比较。此外,如果您有一个给定的最大工作人员,则通过将有效延迟增加到底层系统之上(假设系统可以真正并行执行任意多个进程)来改变分析。

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

【讨论】:

    猜你喜欢
    • 2022-01-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-17
    • 1970-01-01
    • 1970-01-01
    • 2013-06-16
    • 2012-01-16
    相关资源
    最近更新 更多