关于python:列表是否是线程安全的?

Are lists thread-safe?

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


列表本身是线程安全的。在cpython中,gil防止并发访问它们,而其他实现则注意为它们的列表实现使用细粒度锁或同步数据类型。但是,尽管列表本身不能通过尝试并发访问而损坏,但列表的数据不受保护。例如:

1
L[0] += 1

如果另一个线程执行相同的操作,则不能保证实际增加l[0],因为EDOCX1[0]不是原子操作。(非常少的操作实际上是原子操作,因为大多数操作都会导致调用任意的python代码。)您应该使用队列,因为如果只使用未受保护的列表,则可能会因为争用条件而获取或删除错误的项。


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

这是因为一旦我们开始写入数据,就不需要担心正在读取的数据将在同一位置。append()操作不读取数据,只将数据写入列表。


这里有一个全面但非详尽的list操作示例列表,以及它们是否是线程安全的。希望得到关于obj in a_list语言结构的答案。


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

有缺陷的版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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)

出错时输出

1
2
3
4
5
6
7
8
9
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

使用锁的版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import threading
import time
count = 1000
l = []
r = threading.RLock()
def add():
    r.acquire()
    for i in range(count):
        l.append(i)
        time.sleep(0.0001)
    r.release()

def remove():
    r.acquire()
    for i in range(count):
        l.remove(i)
        time.sleep(0.0001)
    r.release()


t1 = threading.Thread(target=add)
t2 = threading.Thread(target=remove)
t1.start()
t2.start()
t1.join()
t2.join()

print(l)

产量

1
[] # Empty list

结论

正如前面的回答中所提到的,当从列表中附加或弹出元素的行为本身是线程安全的时,不线程安全的是当您在一个线程中附加并在另一个线程中弹出时。