如果我有一个整数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
答案 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