列表是线程安全的吗?

217

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


4
很难总是确定在Python中确切保证了什么是线程安全的,而且很难理解其中的线程安全性。即使是备受欢迎的比特币钱包Electrum也可能存在并发错误,这很可能源于此。 - sudo
4个回答

260

列表本身是线程安全的。在CPython中,GIL保护它们免受并发访问的影响,而其他实现则采用细粒度锁或同步数据类型来处理其列表实现。然而,尽管列表本身不能因并发访问尝试而损坏,但列表的数据却没有受到保护。例如:

L[0] += 1

如果另一个线程也执行相同的操作,+= 并不能保证会真正将 L[0] 增加一,因为该操作并不是原子操作(在 Python 中非常少数的操作是原子操作,因为其中大多数操作都可能导致调用任意 Python 代码)。你应该使用队列(Queues),因为如果你只使用未受保护的列表,由于竞态条件可能会获取或删除错误的项目


2
deque也是线程安全的吗?它似乎更适合我的使用。 - lemiant
26
所有的 Python 对象都具有相同类型的线程安全性——它们本身不会损坏,但它们的数据可能会。collections.deque 是 Queue.Queue 对象背后的实现。如果你从两个线程访问同一对象,你真的应该使用 Queue.Queue 对象。真的。 - Thomas Wouters
10
lemiant,deque 是线程安全的。来自《流畅的Python》第2章的内容:“collections.deque 类是一个线程安全的双端队列,旨在快速地从两端插入和删除元素。[...] append 和 popleft 操作是原子操作,因此 deque 在多线程应用中可以作为 LIFO(后进先出)队列使用,无需使用锁。” - Al Sweigart
4
这个回答是关于CPython还是Python本身的?针对Python本身的答案是什么? - user541686
@Nils: 嗯,您链接的第一页说的是Python而不是CPython,因为它确实在描述Python语言。而第二个链接字面上则说有多个Python语言的实现,只是其中一个更受欢迎。考虑到问题是关于Python的,答案应该描述在任何符合Python标准的实现中可以保证发生什么,而不仅仅是在特定的CPython中会发生什么。 - user541686
1
@Mehrdad 你想讨论这个话题吗?你说你不想,现在似乎改变了主意。好的,对于你的问题的答案是:对于Python和CPython的答案是相同的,因为这是参考实现。是的,了解其他实现方式可能会有所帮助,但你的问题是“这个答案是关于CPython还是Python?”答案是:两者都是。Python并不是其他实现方式,它只是CPython。 - Nils Lindemann

143
为了澄清Thomas的优秀回答中的一个观点,应该提到append()是线程安全的。
这是因为我们写入数据时不用担心正在读取的数据会在同一位置。 append()操作不会读取数据,它只会向列表中写入数据。

1
PyList_Append从内存中读取。您是否意味着它的读写是在同一个GIL锁中发生的?https://github.com/python/cpython/blob/1cecdbbf1adef9af2ac829c7dbba91352accb1fd/Objects/listobject.c#L303 - amwinter
1
@amwinter 是的,对 PyList_Append 的整个调用都在一个 GIL 锁中完成。它会被给予一个要添加的对象的引用。在评估完它并调用 PyList_Append 之前,该对象的内容可能会改变。但它仍然是同一个对象,并且安全地被附加(如果你执行 lst.append(x); ok = lst[-1] is x,那么 ok 可能会是 False)。 你所引用的代码除了增加对该对象的引用计数外,并不从被添加的对象中读取任何内容。它读取并可能重新分配被添加到其中的列表。 - greggo
3
dotancohen的观点是,L[0] += x 会在 L 上执行一个 __getitem__,然后在 L 上执行一个 __setitem__ -- 如果 L 支持 __iadd__,它会在对象接口上以略微不同的方式执行,但在Python解释器层面仍然有两个独立的操作(你可以在编译的字节码中看到它们)。而 append 在字节码中只需调用一个方法即可完成。 - greggo
1
这很有帮助。我知道 li.append(item) 是线程安全的,但我认为 li += [item] 不是线程安全的,对吗? - speedplane
13
"remove"怎么样? - acrazing
显示剩余2条评论

55
以下是关于list操作的全面但不穷尽的示例列表,以及它们是否线程安全:点击这里查看。希望在此处获得关于obj in a_list语言结构的答案。请注意,保留了HTML标签,并且没有解释性内容。

6
我万万没想到list.sort() 是原子操作,一开始我持怀疑态度,但我进行了测试,结果证明是真的。当一个线程开始对一个包含1亿个元素的巨大列表进行排序时,它会阻塞所有其他线程访问该列表(我有另一个线程不断地检索元素0,但它在A线程排序时挂起了几秒钟)。所以我想这是真的,并且已经在Python 3.9.1中验证过了。 - A Kareem
它被包含在Python FAQ中。 - bartolo-otrit

-3

最近我遇到了这样一个情况,需要在一个线程中不断地向列表中添加元素,循环遍历这些元素并检查它们是否准备好,如果是 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

结论

正如早先的回答所提到的,尽管从列表中添加或弹出元素本身是线程安全的行为,但在一个线程中添加,在另一个线程中弹出是不安全的。


13
有锁的版本与没有锁的版本具有相同的行为。基本上,错误是因为它试图删除不在列表中的内容,这与线程安全无关。尝试在更改启动顺序即在t1之前启动t2后运行带锁版本,您将看到相同的错误。无论是否使用锁定,只要t2领先于t1,错误就会发生。 - Dev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接