扩展Python列表(例如l + = [1])保证是线程安全的吗?

时间:2016-07-08 12:04:04

标签: python multithreading thread-safety python-multithreading

如果我有一个整数i,在多个线程上执行i += 1是不安全的:

>>> i = 0
>>> def increment_i():
...     global i
...     for j in range(1000): i += 1
...
>>> threads = [threading.Thread(target=increment_i) for j in range(10)]
>>> for thread in threads: thread.start()
...
>>> for thread in threads: thread.join()
...
>>> i
4858  # Not 10000

但是,如果我有一个列表l,在多个线程上执行l += [1]似乎是安全的:

>>> l = []
>>> def extend_l():
...     global l
...     for j in range(1000): l += [1]
...
>>> threads = [threading.Thread(target=extend_l) for j in range(10)]
>>> for thread in threads: thread.start()
...
>>> for thread in threads: thread.join()
...
>>> len(l)
10000

l += [1]是否保证是线程安全的?如果是这样,这适用于所有Python实现还是只适用于CPython?

修改l += [1]似乎是线程安全的,但l = l + [1]不是......

>>> l = []
>>> def extend_l():
...     global l
...     for j in range(1000): l = l + [1]
...
>>> threads = [threading.Thread(target=extend_l) for j in range(10)]
>>> for thread in threads: thread.start()
...
>>> for thread in threads: thread.join()
...
>>> len(l)
3305  # Not 10000

2 个答案:

答案 0 :(得分:15)

没有幸福;-)答案。没有任何保证,只需注意Python参考手册不保证原子性,就可以确认。

在CPython中,这是一个语用学问题。正如effbot的一篇文章所说,

  

理论上,这意味着精确的会计需要准确理解PVM [Python虚拟机]字节码的实现。

这就是事实。 CPython专家知道L += [x]是原子的,因为他们知道以下所有内容:

  • +=编译为INPLACE_ADD字节码。
  • 列表对象INPLACE_ADD的实现完全用C语言编写(执行路径上没有Python代码,因此无法在字节码之间发布GIL)。
  • listobject.c中,INPLACE_ADD的实现是函数list_inplace_concat(),在执行过程中没有任何内容需要执行任何用户Python代码(如果有的话,可能会再次发布GIL) )。

这可能听起来非常难以保持直线,但对于那些有了CPython内部知识的人(当时他写的那篇文章),实际上并非如此。事实上,鉴于知识的深度,这一切都很明显; - )

因此,作为 pragmatics 的问题,CPython专家总是自由地依赖于“看起来像原子的操作应该是原子的”,并且还指导了一些语言决策。例如,在effbot列表中缺少一个操作(在他撰写该文章后添加到该语言中):

x = D.pop(y) # or ...
x = D.pop(y, default)

支持添加dict.pop()的一个论点(当时)恰恰是明显的C实现是原子的,而在使用中(当时)替代:

x = D[y]
del D[y]

不是原子的(检索和删除是通过不同的字节码完成的,因此线程可以在它们之间切换)。

但是文档永远不会 .pop()是原子的,永远不会。这是一种“同意成年人”的事情:如果你足够专业,可以故意利用这一点,你就不需要手持了。如果你不够专业,那么effbot文章的最后一句适用:

  

如有疑问,请使用互斥锁!

作为务实的必要性,核心开发人员永远不会在CPython中打破effbot示例(或D.pop()D.setdefault())的原子性。但是,其他实现完全没有义务模仿这些实用的选择。实际上,由于这些情况下的原子性依赖于CPython特定形式的字节码结合CPython使用只能在字节码之间释放的全局解释器锁,因此可能对于模仿它们的其他实现来说真的很痛苦

你永远不会知道:CPython的未来版本也可能删除GIL!我对此表示怀疑,但这在理论上是可行的。但如果发生这种情况,我敢打赌保留GIL的并行版本也会得到维护,因为很多代码(特别是用C编写的扩展模块)也依赖于GIL来保证线程安全。

值得重复:

  

如有疑问,请使用互斥锁!

答案 1 :(得分:9)

来自http://effbot.org/pyfaq/what-kinds-of-global-value-mutation-are-thread-safe.htm

  

替换其他对象的操作可能会在引用计数达到零时调用其他对象的R$layout.class方法,这会影响事物。对字典和列表的批量更新尤其如此。

     

以下操作都是原子的(L,L1,L2是列表,D,D1,D2是dicts,x,y是对象,i,j是整数):

__del__
     

这些不是:

L.append(x)
L1.extend(L2)
x = L[i]
x = L.pop()
L1[i:j] = L2
L.sort()
x = y
x.field = y
D[x] = y
D1.update(D2)
D.keys()

上面是纯粹的CPython特定的,可以在不同的Python实现中有所不同,例如PyPy。

顺便提一下,有一个用于记录原子Python操作的问题 - https://bugs.python.org/issue15339