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


当前回答

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

L[0] += 1

如果另一个线程做同样的事情,并不保证L[0]实际增加1,因为+=不是一个原子操作。(Python中非常非常少的操作实际上是原子的,因为它们中的大多数会导致任意的Python代码被调用。)您应该使用队列,因为如果您只是使用一个不受保护的列表,您可能会因为竞争条件而获取或删除错误的项。

其他回答

下面是一个全面但不详尽的列表操作示例列表,以及它们是否线程安全。 希望在a_list语言构造中得到关于obj的答案。

I recently had this case where I needed to append to a list continuously in one thread, loop through the items and check if the item was ready, it was an AsyncResult in my case and remove it from the list only if it was ready. I could not find any examples that demonstrated my problem clearly Here is an example demonstrating adding to list in one thread continuously and removing from the same list in another thread continuously The flawed version runs easily on smaller numbers but keep the numbers big enough and run a few times and you will see the error

错误的版本

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

结论

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

为了澄清Thomas的精彩回答中的一点,应该提到append()是线程安全的。

这是因为当我们写入数据时,不需要担心正在读取的数据是否会在相同的位置。append()操作不读取数据,它只将数据写入列表。

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

L[0] += 1

如果另一个线程做同样的事情,并不保证L[0]实际增加1,因为+=不是一个原子操作。(Python中非常非常少的操作实际上是原子的,因为它们中的大多数会导致任意的Python代码被调用。)您应该使用队列,因为如果您只是使用一个不受保护的列表,您可能会因为竞争条件而获取或删除错误的项。