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

扩展Python列表(例如l+;=[1])是否保证线程安全?,python,multithreading,thread-safety,python-multithreading,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 thre

如果我有一个整数
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 = []
>>> 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
,那么在多个线程上执行
l+=[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 = []
>>> 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
发件人:

当其他对象的引用计数为零时,替换其他对象的操作可能会调用这些其他对象的
\uu del\uu
方法,这可能会影响一些事情。这对于字典和列表的大规模更新尤其如此

以下操作均为原子操作(L、L1、L2为列表,D、D1、D2为指令,x、y为对象,i、j为整数):

这些不是:

i = i+1
L.append(L[-1])
L[i] = L[j]
D[x] = D[x] + 1
以上内容纯粹是特定于CPython的,并且可以在不同的Python实现(如PyPy)中有所不同

顺便说一句,记录原子Python操作还有一个公开的问题-

没有一个快乐的;-)答案是这样的。其中任何一个都没有保证,您只需注意Python参考手册没有保证原子性就可以证实这一点

在皮顿,这是一个语用学的问题。正如effbot文章的一部分所说

从理论上讲,这意味着准确的记帐需要准确理解PVM[Python虚拟机]字节码实现

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

  • +=
    编译为
    in place\u ADD
    字节码
  • 列表对象的
    INPLACE\u ADD
    的实现完全用C编写(执行路径上没有Python代码,因此无法在字节码之间释放GIL)
  • listobject.c
    中,
    INPLACE\u ADD
    的实现是函数
    list\u INPLACE\u concat()
    ,执行过程中也不需要执行任何用户Python代码(如果执行了,GIL可能会再次被释放)
这听起来可能很难做到直截了当,但对于一个了解effbot的CPython的内部结构(在他写那篇文章的时候)的人来说,事实并非如此。事实上,鉴于知识的深度,这是显而易见的;-)

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

支持添加
dict.pop()
的一个论点(当时)正是显而易见的C实现将是原子的,而正在使用的替代方案(当时)是:

x = D[y]
del D[y]
was不是原子的(检索和删除是通过不同的字节码完成的,因此线程可以在它们之间切换)

但是文档从来没有说过
.pop()
是原子的,也永远不会。这是一种“成年人同意”的事情:如果你足够专业,能够在知情的情况下利用这一点,你就不需要牵手。如果你不够专业,那么effbot文章的最后一句话适用:

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

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

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

值得重复的是:

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


这对我来说真是令人惊讶——我不希望这样。我希望有人能对此给出一个清晰的解释。尽管我对此投了赞成票,但我认为“Python中哪些操作保证线程安全,哪些操作不保证线程安全?”这句话谴责了这个问题。你能换一种说法吗?在等待对这个问题增加一些限制之后,我又找到了effBot:一本有趣的读物。我建议将“什么样的全局值突变是线程安全的”改写成一个好的危险;-)关于列表示例:列表在其操作中是线程安全的,但数据本身不受容器的“保护”。因此,对列表更改元素内容的任何访问都将受到整数
+=1
@MartijnPieters的影响-我假设您已经结束了这个问题,因为“Python中的哪些操作…”语句。我现在已经删除了这个概括——你愿意重新开始这个问题吗?@user200783:done;请尽量让你的问题具体化。OP还询问了其他Python实现,我也很想知道。。。GIL是否在所有实现(实际上有GIL)上对所有这些操作都起相同的作用?感谢您提供这些链接。第15339期已经有4年历史了——似乎没有人急于用Python记录关于线程安全的任何内容。该语言的这个基本属性仍然是其参考实现的一个实现细节。感谢您提供了这个全面的答案。总结似乎是“要么使用互斥,要么依赖于未记录的CPython实现细节”。之前