列表是线程安全的吗?


154

我注意到经常建议使用具有多个线程的队列,而不是列表和.pop()。这是因为列表不是线程安全的,还是出于其他原因?


1
很难总是说出在Python中到底可以保证线程安全的是什么,并且很难在其中推理出线程安全性。即使是广受欢迎的比特币钱包Electrum也可能存在与此相关的并发错误。
sudo

Answers:


181

列表本身是线程安全的。在CPython中,GIL防止对它们的并发访问,而其他实现则要小心地为它们的列表实现使用细粒度锁或同步数据类型。但是,虽然列表本身不会因尝试并发访问而损坏,但是列表的数据不受保护。例如:

L[0] += 1

如果另一个线程做同样的事情,则不能保证实际上将L [0]增加一,因为 +=这不是原子操作。(实际上,Python中很少有原子操作的操作,因为它们中的大多数操作都会导致调用任意Python代码。)您应该使用Queues,因为如果您仅使用不受保护的列表,则可能由于种族而获得或删除了错误的项目条件。


1
双端队列也是线程安全的吗?似乎更适合我使用。
2011年

20
所有Python对象都具有相同的线程安全性-它们本身不会损坏,但它们的数据可能会损坏。collections.deque是Qu​​eue.Queue对象的背后。如果要从两个线程访问事物,则实际上应该使用Queue.Queue对象。真。
托马斯·沃特斯

10
优雅,双端队列是线程安全的。从Fluent Python的第2章:“类collections.deque是线程安全的双端队列,旨在快速从两端插入和删除。[...] append和popleft操作是原子的,因此deque是安全的在多线程应用程序中用作LIFO队列,而无需使用锁。”
阿尔·斯威加特

3
这个答案是关于CPython还是关于Python?Python本身的答案是什么?
user541686 '18

@Nils:呃,你链接到第一页说的Python,而不是CPython的,因为它描述了Python语言。第二个链接从字面上说是Python语言的多种实现,只是其中一种更流行。考虑到问题是关于Python的,答案应该描述在任何一致的Python实现中可以保证发生的事情,而不仅仅是CPython中发生的事情。
user541686 '19

89

为了澄清托马斯出色答案的观点,应该提到的append() 线程安全的。

这是因为不必担心一旦我们去写入数据,读取的数据就会位于同一位置。该操作不读取数据,仅将数据写入列表。append()


1
PyList_Append正在从内存中读取。您是说它的读写发生在同一个GIL锁中吗?github.com/python/cpython/blob/…–
amwinter

1
@amwinter是的,整个调用PyList_Append都在一个GIL锁中完成。它为要附加的对象提供了参考。该对象的内容在评估后和调用PyList_Append完成之前可能会更改。但是它仍将是同一对象,并安全地附加(如果这样做lst.append(x); ok = lst[-1] is x,则ok可能为False)。您引用的代码不会从附加对象中读取,除非将其INCREF。它读取并可以重新分配附加到的列表。
greggo,

2
dotancohen的一点是,L[0] += x将执行__getitem__L,然后__setitem__L-如果L支持__iadd__将在物体界面做不同的事情了一下,但还是有两个独立的操作上L,在Python解释器级别(你会看到他们在编译的字节码)。这append是通过字节码中的单个方法调用完成的。
greggo,2013年

6
怎么remove
1998年

2
赞!所以我可以连续追加一个线程并弹出另一个线程吗?
PirateApp '19


3

我最近遇到过这种情况,我需要在一个线程中连续地追加到列表,循环遍历这些项目,并检查该项目是否准备就绪,在我的情况下,它是一个AsyncResult,只有在准备就绪时才将其从列表中删除。我找不到任何示例可以清楚地说明我的问题。这是一个示例,该示例演示了在一个线程中连续添加到列表,并在另一个线程中连续从同一列表中删除该有缺陷的版本很容易在较小的数字上运行,但保持足够大的数字并运行一个几次,你会看到错误

FLAWED版本

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

结论

如前面的回答中所述,虽然从列表本身追加或弹出元素的行为是线程安全的,但当您在一个线程中追加并在另一个线程中弹出时,不是线程安全的


5
有锁的版本与没有锁的版本具有相同的行为。基本上,该错误即将到来,因为它正尝试删除列表中未包含的内容,这与线程安全无关。尝试更改启动顺序后(即在t1之前启动t2),然后带锁运行该版本,您将看到相同的错误。每当t2领先于t1时,无论是否使用锁,都会发生错误。
开发

1
另外,最好还是使用上下文管理器(with r:)而不是显式调用r.acquire()andr.release()
GordonAitchJay

1
@GordonAitchJay👍–
蒂莫西·奎因
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.