我注意到通常建议使用具有多个线程的队列,而不是列表和.pop()
。这是因为列表不是线程安全的,还是由于其他原因?
我注意到通常建议使用具有多个线程的队列,而不是列表和.pop()
。这是因为列表不是线程安全的,还是由于其他原因?
列表本身是线程安全的。在CPython中,GIL保护它们免受并发访问的影响,而其他实现则采用细粒度锁或同步数据类型来处理其列表实现。然而,尽管列表本身不能因并发访问尝试而损坏,但列表的数据却没有受到保护。例如:
L[0] += 1
如果另一个线程也执行相同的操作,+=
并不能保证会真正将 L[0] 增加一,因为该操作并不是原子操作(在 Python 中非常少数的操作是原子操作,因为其中大多数操作都可能导致调用任意 Python 代码)。你应该使用队列(Queues),因为如果你只使用未受保护的列表,由于竞态条件可能会获取或删除错误的项目。
append()
是线程安全的。append()
操作不会读取数据,它只会向列表中写入数据。PyList_Append
的整个调用都在一个 GIL 锁中完成。它会被给予一个要添加的对象的引用。在评估完它并调用 PyList_Append
之前,该对象的内容可能会改变。但它仍然是同一个对象,并且安全地被附加(如果你执行 lst.append(x); ok = lst[-1] is x
,那么 ok
可能会是 False)。
你所引用的代码除了增加对该对象的引用计数外,并不从被添加的对象中读取任何内容。它读取并可能重新分配被添加到其中的列表。 - greggoL[0] += x
会在 L
上执行一个 __getitem__
,然后在 L
上执行一个 __setitem__
-- 如果 L
支持 __iadd__
,它会在对象接口上以略微不同的方式执行,但在Python解释器层面仍然有两个独立的操作(你可以在编译的字节码中看到它们)。而 append
在字节码中只需调用一个方法即可完成。 - greggoli.append(item)
是线程安全的,但我认为 li += [item]
不是线程安全的,对吗? - speedplanePython 3.9.1
中验证过了。 - A Kareem最近我遇到了这样一个情况,需要在一个线程中不断地向列表中添加元素,循环遍历这些元素并检查它们是否准备好,如果是 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
结论
正如早先的回答所提到的,尽管从列表中添加或弹出元素本身是线程安全的行为,但在一个线程中添加,在另一个线程中弹出是不安全的。