我注意到,通常建议使用多线程队列,而不是列表和.pop()。这是因为列表不是线程安全的,还是因为其他原因?
很难说在Python中什么是保证线程安全的,很难解释其中的线程安全性。即使是极受欢迎的比特币钱包Electrum也有可能由此产生并发错误。
列表本身是线程安全的。在cpython中,gil防止并发访问它们,而其他实现则注意为它们的列表实现使用细粒度锁或同步数据类型。但是,尽管列表本身不能通过尝试并发访问而损坏,但列表的数据不受保护。例如:
L[0] += 1
如果另一个线程执行相同的操作,则不能保证实际增加l[0],因为EDOCX1[0]不是原子操作。(非常少的操作实际上是原子操作,因为大多数操作都会导致调用任意的python代码。)您应该使用队列,因为如果只使用未受保护的列表,则可能会因为争用条件而获取或删除错误的项。
德克也安全吗?它似乎更适合我使用。
所有Python对象都具有相同的线程安全性——它们本身不会损坏,但它们的数据可能会损坏。collections.deque是queue.queue对象后面的内容。如果您从两个线程访问内容,那么您真的应该使用queue.queue对象。真的?
莱米安,德克是线程安全。在fluent python的第2章中:"class collections.deque是一个线程安全的双端队列,设计用于从两端快速插入和删除。[…]APPEND和POPLEFT操作是原子操作,因此在多线程应用程序中可以安全地将DEQUE用作后进先出队列,而无需使用锁。"
这个答案是关于cpython还是关于python?Python本身的答案是什么?
@尼尔斯:嗯,你链接到的第一个页面是python而不是cpython,因为它描述的是python语言。第二个链接字面意思是有多个Python语言的实现,恰好有一个更流行。考虑到这个问题是关于Python的,答案应该描述可以保证在任何符合规范的Python实现中发生的事情,而不仅仅是在cpython中发生的事情。
@你想不想讨论这个话题?你说,你不想。看来你改变主意了。好啊。您的问题的答案是:Python的答案与cpython的答案相同,因为这是引用实现。是的,它可能对在其他实现中如何实现感兴趣,但您的问题是这个答案是关于cpython还是关于python?答案是:两者都有。python不是其他实现。只不过是塞顿。
为了澄清托马斯出色的回答中的一点,应该提到append()是线程安全的。
这是因为一旦我们开始写入数据,就不需要担心正在读取的数据将在同一位置。append()操作不读取数据,只将数据写入列表。
pylist_append正在从内存中读取。你的意思是它的读写发生在同一个gil锁中吗?github.com/python/cpython/blob/…
@阿姆温特是的,打给埃多克斯的整个电话都是在一个金库里完成的。它提供了对要附加的对象的引用。该对象的内容可能会在评估后和调用PyList_Append之前更改。但它仍然是同一个对象,并安全地附加(如果您执行lst.append(x); ok = lst[-1] is x,那么ok当然可能是错误的)。您引用的代码不会从附加的对象中读取,除非增加它的值。它读取并可能重新分配附加到的列表。
Dotankohen的观点是,L[0] += x将在L上执行__getitem__,然后在L上执行__setitem__--如果L支持__iadd__它在对象接口上执行的操作略有不同,但在python解释器级别上L上仍有两个独立的操作(您将在中看到它们编译的字节码)。append是在字节码中的单个方法调用中完成的。
这很有帮助。我知道li.append(item)是threadsafe,但我想li += [item]不是threadsafe,对吧?
埃多克斯1〔15〕怎么样?
@Greggo听起来像是当前的cpython实现,而不是python。例如,Pypy的情况也一样吗?
@罗伯特·格兰特,我的评论确实是关于塞顿的。我不知道其他实现。
恭喜!那么,我可以连续地追加到一个线程中并弹出另一个线程吗?
这里有一个全面但非详尽的list操作示例列表,以及它们是否是线程安全的。希望得到关于obj in a_list语言结构的答案。
第一个链接似乎已断开。
我最近遇到了这样的情况,我需要在一个线程中连续地追加到一个列表,循环遍历这些项并检查该项是否已就绪,在我的情况下它是一个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"", line 13, in remove
l.remove(i)
ValueError: list.remove(x): x not in list
使用锁的版本
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)
产量
[] # Empty list
结论
正如前面的回答中所提到的,当从列表中附加或弹出元素的行为本身是线程安全的时,不线程安全的是当您在一个线程中附加并在另一个线程中弹出时。