在下面的例子中使用list(d.items())
是否安全?
import threading
n = 2000
d = {}
def dict_to_list():
while True:
list(d.items()) # is this safe to do?
def modify():
for i in range(n):
d[i] = i
if __name__ == "__main__":
t1 = threading.Thread(target=dict_to_list, daemon=True)
t1.start()
t2 = threading.Thread(target=modify, daemon=True)
t2.start()
t2.join()
这个问题背后的背景是,字典项视图上的迭代器在每一步检查字典大小是否改变,如下面的例子所示。
d = {}
view = d.items() # this is an iterable
it = iter(view) # this is an iterator
d[1] = 1
print(list(view)) # this is ok, it prints [(1, 1)]
print(list(it)) # this raises a RuntimeError because the size of the dictionary changed
因此,如果上面第一个示例中对list(...)
的调用可以被中断(即,线程t1
可以释放GIL),那么第一个示例可能会导致线程t1
中出现RuntimeErrors。有消息称该操作不是原子操作,请参阅此处。然而,我还没能让第一个例子崩溃。
我理解这里安全的做法是使用一些锁,而不是试图依赖于某些操作的原子性。但是,我正在调试第三方库中的一个问题,该库使用类似的代码,并且我不能直接更改。
简短的回答:这可能是好的,但无论如何要使用锁。
使用dis
你可以看到list(d.items())
实际上是两个字节码指令(6
和8
):
>>> import dis
>>> dis.dis("list(d.items())")
1 0 LOAD_NAME 0 (list)
2 LOAD_NAME 1 (d)
4 LOAD_METHOD 2 (items)
6 CALL_METHOD 0
8 CALL_FUNCTION 1
10 RETURN_VALUE
在Python FAQ中,它说(通常)用C实现的东西是原子的(从运行Python程序的角度来看):
什么类型的全局值突变是线程安全的?
一般来说,Python只提供在字节码指令之间在线程之间切换;[…]。因此,从Python程序的角度来看,每个字节码指令以及从每个指令到达的所有C实现代码都是原子的。
[…]
例如,下列操作都是原子[…]
D.keys()
list()
是用C实现的,d.items()
是用C实现的,所以每个都应该是原子的,除非它们最终以某种方式调用Python代码(如果它们调用你使用Python实现覆盖的dunder方法可能会发生),或者如果你使用dict
的子类而不是真正的dict
,或者如果他们的C实现释放GIL。依赖它们是原子性的不是一个好主意。
你提到,如果iter()
的底层可迭代对象改变大小,它会出错,但这与这里无关,因为.keys()
、.values()
和.items()
返回一个视图对象,而这些对象在底层对象改变时没有问题:
d = {"a": 1, "b": 2}
view = d.items()
print(list(view)) # [("a", 1), ("b", 2)]
d["c"] = 3 # this could happen in a different thread
print(list(view)) # [("a", 1), ("b", 2), ("c", 3)]
如果你一次在多个指令中修改字典,你有时会得到d
处于不一致的状态,有些修改已经完成,有些还没有,但是你不应该得到RuntimeError
,就像你对iter()
那样,除非你以一种非原子的方式修改它。
我怀疑那篇文章的作者对字典视图感到困惑,认为dict.items
返回一个iterator像Python 2中的dict.iteritems
一样,不是iterable就像在Python 3中一样。请注意,这篇文章写于13年前,比Python 3.0发布早了5个月。顺便说一句,正如PEP 3106所说(强调我的):
Python 2,原始计划是简单地让。keys(),。values()和。items()返回iterator,即与Python 2.x中iterkeys(), itervalues()和iteritems()的返回值完全相同。
iteritems
给出iterator:>>> d = {1: 1, 2: 2, 3: 3}
>>> items = d.iteritems()
>>> items
<dictionary-itemiterator object at 0x0000000003EBA958>
>>> next(items)
(1, 1)
>>> list(items)
[(2, 2), (3, 3)]
>>> list(items)
[]
Python 3,items
给出了一个iterable,不是iterator:>>> d = {1: 1, 2: 2, 3: 3}
>>> items = d.items()
>>> items
dict_items([(1, 1), (2, 2), (3, 3)])
>>> next(items)
Traceback (most recent call last):
File "<pyshell#9>", line 1, in <module>
next(items)
TypeError: 'dict_items' object is not an iterator
>>> list(items)
[(1, 1), (2, 2), (3, 3)]
>>> list(items)
[(1, 1), (2, 2), (3, 3)]
在Python 2中,使用iterator,这会导致错误:
>>> d = {1: 1, 2: 2, 3: 3}
>>> items = d.iteritems()
>>> d[4] = 4
>>> next(items)
Traceback (most recent call last):
File "<pyshell#26>", line 1, in <module>
next(items)
RuntimeError: dictionary changed size during iteration
在Python 3中,如果d.items()
确实返回一个迭代器,也就是说,如果它等于iter(d.items())
,那么它将是不安全的。因为您的线程可能在iter()
创建迭代器和list()
使用迭代器之间被中断。但是由于它返回iterable,是list()
函数本身在内部创建了一个iterator因此,迭代器的创建和使用都发生在同一条单字节码指令(执行list()
函数)期间。
如果您将代码更改为list(iter(d.items()))
并将n
增加为20000000
,那么您可能会得到错误。从运行的例子试试它在线!:
Exception in thread Thread-1:
Traceback (most recent call last):
File "/usr/lib64/python3.8/threading.py", line 932, in _bootstrap_inner
self.run()
File "/usr/lib64/python3.8/threading.py", line 870, in run
self._target(*self._args, **self._kwargs)
File ".code.tio", line 9, in dict_to_list
list(iter(d.items())) # is this safe to do?
RuntimeError: dictionary changed size during iteration