扩展 Python 列表(例如 l += [1])是否保证是线程安全的?
Is extending a Python list (e.g. l += [1]) guaranteed to be thread-safe?
如果我有一个整数 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
来自 http://effbot.org/pyfaq/what-kinds-of-global-value-mutation-are-thread-safe.htm :
Operations that replace other objects may invoke those other objects’ __del__
method when their reference count reaches zero, and that can affect things. This is especially true for the mass updates to dictionaries and lists.
The following operations are all atomic (L, L1, L2 are lists, D, D1, D2 are dicts, x, y are objects, i, j are ints):
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()
These aren’t:
i = i+1
L.append(L[-1])
L[i] = L[j]
D[x] = D[x] + 1
以上内容纯粹是 CPython 特定的,可能因 PyPy 等不同 Python 实现而异。
顺便说一句,有一个未解决的问题用于记录原子 Python 操作 - https://bugs.python.org/issue15339
对此没有一个满意的答案 ;-)。没有任何保证,您只需注意 Python 参考手册不保证原子性即可确认这一点。
在 CPython 中这是一个语用问题。正如 effbot 文章的摘录部分所说,
In theory, this means an exact accounting requires an exact understanding of the PVM [Python Virtual Machine] bytecode implementation.
这是事实。 CPython 专家知道 L += [x]
是原子的,因为他们知道以下所有内容:
+=
编译为 INPLACE_ADD
字节码。
- 列表对象
INPLACE_ADD
的实现完全是用C写的(执行路径上没有Python代码,所以GIL无法释放在[之间] =58=]字节码)。
- 在
listobject.c
中,INPLACE_ADD
的实现是函数list_inplace_concat()
,在执行期间也不需要执行任何用户Python代码(如果需要, GIL 可能会再次发布)。
这听起来可能很难保持直截了当,但对于像 effbot 那样了解 CPython 内部结构的人(在他写那篇文章的时候)来说,事实并非如此。事实上,考虑到知识的深度,这一切都是显而易见的 ;-)
因此,作为 语用学,CPython 专家一直自由地依赖 "operations that 'look atomic' should really be atomic",这也指导了一些语言决策。例如,effbot 的列表中缺少一个操作(他写了那篇文章后添加到语言中):
x = D.pop(y) # or ...
x = D.pop(y, default)
支持添加 dict.pop()
的一个论点(当时)恰恰是明显的 C 实现将是原子的,而正在使用的(当时)替代方案:
x = D[y]
del D[y]
不是原子的(检索和删除是通过不同的字节码完成的,因此线程可以在它们之间切换)。
但文档从未说过 .pop()
是原子的,将来也不会。这是一种 "consenting adults" 的东西:如果你足够专业,可以有意识地利用它,你就不需要亲自动手。如果你不够专业,那么effbot文章的最后一句话适用:
When in doubt, use a mutex!
出于实际需要,核心开发人员永远不会破坏 CPython 中 effbot 示例(或 D.pop()
或 D.setdefault()
)的原子性。不过,其他实现根本没有义务模仿这些务实的选择。实际上,由于在这些情况下的原子性依赖于 CPython 的特定形式的字节码以及 CPython 对只能在字节码之间释放的全局解释器锁的使用,它 可能 其他实现模仿它们真的很痛苦。
你永远不知道:CPython 的某些未来版本也可能会删除 GIL!我对此表示怀疑,但理论上是可能的。但如果发生这种情况,我敢打赌保留 GIL 的并行版本也会得到维护,因为大量代码(尤其是用 C
编写的扩展模块)也依赖 GIL 来实现线程安全。
值得重复:
When in doubt, use a mutex!
如果我有一个整数 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
来自 http://effbot.org/pyfaq/what-kinds-of-global-value-mutation-are-thread-safe.htm :
Operations that replace other objects may invoke those other objects’
__del__
method when their reference count reaches zero, and that can affect things. This is especially true for the mass updates to dictionaries and lists.The following operations are all atomic (L, L1, L2 are lists, D, D1, D2 are dicts, x, y are objects, i, j are ints):
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()
These aren’t:
i = i+1 L.append(L[-1]) L[i] = L[j] D[x] = D[x] + 1
以上内容纯粹是 CPython 特定的,可能因 PyPy 等不同 Python 实现而异。
顺便说一句,有一个未解决的问题用于记录原子 Python 操作 - https://bugs.python.org/issue15339
对此没有一个满意的答案 ;-)。没有任何保证,您只需注意 Python 参考手册不保证原子性即可确认这一点。
在 CPython 中这是一个语用问题。正如 effbot 文章的摘录部分所说,
In theory, this means an exact accounting requires an exact understanding of the PVM [Python Virtual Machine] bytecode implementation.
这是事实。 CPython 专家知道 L += [x]
是原子的,因为他们知道以下所有内容:
+=
编译为INPLACE_ADD
字节码。- 列表对象
INPLACE_ADD
的实现完全是用C写的(执行路径上没有Python代码,所以GIL无法释放在[之间] =58=]字节码)。 - 在
listobject.c
中,INPLACE_ADD
的实现是函数list_inplace_concat()
,在执行期间也不需要执行任何用户Python代码(如果需要, GIL 可能会再次发布)。
这听起来可能很难保持直截了当,但对于像 effbot 那样了解 CPython 内部结构的人(在他写那篇文章的时候)来说,事实并非如此。事实上,考虑到知识的深度,这一切都是显而易见的 ;-)
因此,作为 语用学,CPython 专家一直自由地依赖 "operations that 'look atomic' should really be atomic",这也指导了一些语言决策。例如,effbot 的列表中缺少一个操作(他写了那篇文章后添加到语言中):
x = D.pop(y) # or ...
x = D.pop(y, default)
支持添加 dict.pop()
的一个论点(当时)恰恰是明显的 C 实现将是原子的,而正在使用的(当时)替代方案:
x = D[y]
del D[y]
不是原子的(检索和删除是通过不同的字节码完成的,因此线程可以在它们之间切换)。
但文档从未说过 .pop()
是原子的,将来也不会。这是一种 "consenting adults" 的东西:如果你足够专业,可以有意识地利用它,你就不需要亲自动手。如果你不够专业,那么effbot文章的最后一句话适用:
When in doubt, use a mutex!
出于实际需要,核心开发人员永远不会破坏 CPython 中 effbot 示例(或 D.pop()
或 D.setdefault()
)的原子性。不过,其他实现根本没有义务模仿这些务实的选择。实际上,由于在这些情况下的原子性依赖于 CPython 的特定形式的字节码以及 CPython 对只能在字节码之间释放的全局解释器锁的使用,它 可能 其他实现模仿它们真的很痛苦。
你永远不知道:CPython 的某些未来版本也可能会删除 GIL!我对此表示怀疑,但理论上是可能的。但如果发生这种情况,我敢打赌保留 GIL 的并行版本也会得到维护,因为大量代码(尤其是用 C
编写的扩展模块)也依赖 GIL 来实现线程安全。
值得重复:
When in doubt, use a mutex!