我注意到通常建议使用具有多个线程的队列,而不是列表和.pop()
. 这是因为列表不是线程安全的,还是出于其他原因?
4 回答
列表本身是线程安全的。在 CPython 中,GIL 防止对它们的并发访问,并且其他实现注意使用细粒度锁或同步数据类型来实现它们的列表。但是,虽然列表本身不会因尝试并发访问而损坏,但列表的数据不受保护。例如:
L[0] += 1
如果另一个线程执行相同的操作,则不能保证实际将 L[0] 增加一个,因为+=
它不是原子操作。(Python 中非常非常少的操作实际上是原子的,因为它们中的大多数都可以导致任意 Python 代码被调用。)您应该使用队列,因为如果您只使用不受保护的列表,您可能会因为竞争而获取或删除错误的项目条件。
为了澄清 Thomas 出色回答中的一点,应该提到的append()
是线程安全的。
这是因为不必担心一旦我们去写入正在读取的数据会在同一个地方。该操作不读取数据,它只将数据写入列表。append()
这是一个全面但非详尽的list
操作示例列表,以及它们是否是线程安全的。希望在这里obj in a_list
得到有关语言结构的答案。
我最近遇到过这种情况,我需要在一个线程中连续追加到列表,循环遍历项目并检查项目是否准备好,在我的情况下它是一个 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)
ERROR时输出
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
结论
正如前面的答案中提到的,虽然从列表本身追加或弹出元素的行为是线程安全的,但当您追加到一个线程并弹出另一个线程时,不是线程安全的