列表是线程安全的吗?
Posted
技术标签:
【中文标题】列表是线程安全的吗?【英文标题】:Are lists thread-safe? 【发布时间】:2011-09-13 05:39:24 【问题描述】:我注意到通常建议使用多线程队列,而不是列表和.pop()
。这是因为列表不是线程安全的,还是出于其他原因?
【问题讨论】:
很难说在 Python 中到底什么是保证线程安全的,而且很难推断其中的线程安全性。即使是广受欢迎的比特币钱包 Electrum 也可能因此而存在并发错误。 【参考方案1】:Here's a comprehensive yet non-exhaustive list of examples 的 list
操作以及它们是否是线程安全的。
希望得到关于obj in a_list
语言结构here的答案。
【讨论】:
一百万年来我从来没有想过 list.sort() 是原子的,我对此表示怀疑,但我测试了它,这是真的,只要一个线程开始对包含 1e8 的巨大列表进行排序它阻止所有其他线程访问列表的元素。 (我有另一个线程不断检索元素 0,它在线程 A 排序时挂起了几秒钟)。所以我猜这是真的,并且在pythong 3.9.1
中得到了验证【参考方案2】:
我最近遇到了这种情况,我需要在一个线程中连续追加到列表,循环遍历项目并检查项目是否准备好,在我的情况下它是一个 AsyncResult 并且只有在它是时才将其从列表中删除准备好。 我找不到任何可以清楚地证明我的问题的例子 这是一个示例,演示连续添加到一个线程中的列表并连续从另一个线程中的同一列表中删除 有缺陷的版本很容易在较小的数字上运行,但保持数字足够大并运行几次,你就会看到错误
有缺陷的版本
import threading
import time
# Change this number as you please, bigger numbers will get the error quickly
count = 1000
l = []
def add():
for i in range(count):
l.append(i)
time.sleep(0.0001)
def remove():
for i in range(count):
l.remove(i)
time.sleep(0.0001)
t1 = threading.Thread(target=add)
t2 = threading.Thread(target=remove)
t1.start()
t2.start()
t1.join()
t2.join()
print(l)
错误时输出
Exception in thread Thread-63:
Traceback (most recent call last):
File "/Users/zup/.pyenv/versions/3.6.8/lib/python3.6/threading.py", line 916, in _bootstrap_inner
self.run()
File "/Users/zup/.pyenv/versions/3.6.8/lib/python3.6/threading.py", line 864, in run
self._target(*self._args, **self._kwargs)
File "<ipython-input-30-ecfbac1c776f>", line 13, in remove
l.remove(i)
ValueError: list.remove(x): x not in list
使用锁的版本
import threading
import time
count = 1000
l = []
lock = threading.RLock()
def add():
with lock:
for i in range(count):
l.append(i)
time.sleep(0.0001)
def remove():
with lock:
for i in range(count):
l.remove(i)
time.sleep(0.0001)
t1 = threading.Thread(target=add)
t2 = threading.Thread(target=remove)
t1.start()
t2.start()
t1.join()
t2.join()
print(l)
输出
[] # Empty list
结论
正如前面的答案中提到的,虽然从列表本身追加或弹出元素的行为是线程安全的,但不是线程安全的是当您追加到一个线程并弹出另一个线程时
【讨论】:
带锁的版本与不带锁的版本具有相同的行为。基本上错误来了,因为它试图删除不在列表中的东西,它与线程安全无关。在更改启动顺序后尝试运行带锁的版本,即在 t1 之前启动 t2,您将看到相同的错误。每当 t2 领先于 t1 时,无论您是否使用锁,都会发生错误。【参考方案3】:为了澄清 Thomas 出色回答中的一点,应该提到 append()
是线程安全的。
这是因为一旦我们去 write 写入数据,就不会担心被 读取 的数据会在同一个地方。 append()
操作不读取数据,它只将数据写入列表。
【讨论】:
PyList_Append 正在从内存中读取。你的意思是它的读取和写入发生在同一个 GIL 锁中吗? github.com/python/cpython/blob/… @amwinter 是的,对PyList_Append
的整个调用都是在一个 GIL 锁中完成的。它被赋予了要附加的对象的引用。该对象的内容可能会在评估之后和对PyList_Append
的调用完成之前更改。但它仍然是同一个对象,并且安全附加(如果你这样做lst.append(x); ok = lst[-1] is x
,那么ok
可能是假的,当然)。您引用的代码不会从附加对象中读取,除了 INCREF 它。它读取并可能重新分配附加到的列表。
dotancohen 的观点是L[0] += x
将在L
上执行__getitem__
,然后在L
上执行__setitem__
——如果L
支持__iadd__
它会做在对象接口上的情况有点不同,但在 python 解释器级别上,L
上仍有两个单独的操作(您将在编译的字节码中看到它们)。 append
在字节码中的单个方法调用中完成。
这很有帮助。我知道li.append(item)
是线程安全的,但我认为li += [item]
不是 线程安全的,对吗?
remove
怎么样?【参考方案4】:
列表本身是线程安全的。在 CPython 中,GIL 防止对它们的并发访问,并且其他实现注意使用细粒度锁或同步数据类型来实现它们的列表。然而,虽然列表自身不会因尝试并发访问而损坏,但列表的数据不受保护。例如:
L[0] += 1
如果另一个线程执行相同的操作,则不能保证实际将 L[0] 增加一个,因为+=
不是原子操作。 (Python 中非常非常少的操作实际上是原子的,因为它们中的大多数都可以导致任意 Python 代码被调用。)您应该使用队列,因为如果您只使用不受保护的列表,您可能会获取或删除错误的项目 因为竞争条件。
【讨论】:
双端队列也是线程安全的吗?它似乎更适合我的使用。 所有 Python 对象都具有相同的线程安全性——它们本身不会损坏,但它们的数据可能会损坏。 collections.deque 是 Queue.Queue 对象背后的东西。如果你从两个线程访问东西,你真的应该使用 Queue.Queue 对象。真的。 lemiant,双端队列是线程安全的。来自 Fluent Python 的第 2 章:“类 collections.deque 是一个线程安全的双端队列,设计用于从两端快速插入和删除。[...] append 和 popleft 操作是原子的,所以 deque 是安全的在多线程应用程序中用作 LIFO 队列,无需使用锁。” 这个答案是关于 CPython 还是关于 Python? Python 本身的答案是什么? @Nils:呃,你链接到的第一页是 Python 而不是 CPython,因为它描述了 Python 语言。第二个链接字面意思是 Python 语言有多种实现,只是碰巧更流行的一种。鉴于问题是关于 Python 的,答案应该描述在任何符合标准的 Python 实现中可以保证发生的事情,而不仅仅是在 CPython 中发生的事情。以上是关于列表是线程安全的吗?的主要内容,如果未能解决你的问题,请参考以下文章