list(dict.items())是线程安全的吗?
是使用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),则第一个示例可能会导致线程中发生 RuntimeErrors t1。有消息称该操作不是原子操作,请参见此处。但是,我无法让第一个示例崩溃。
我知道在这里做的安全事情是使用一些锁而不是试图依赖某些操作的原子性。但是,我正在调试使用类似代码的第三方库中的问题,我不一定直接更改。
回答
简短回答:可能没问题,但无论如何都要使用锁。
使用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 实现代码都是原子的。
[...]
例如,以下操作都是原子的 [...]
>>> 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
list()在 Cd.items()中实现并在 C中实现,所以每个都应该是原子的,除非它们最终以某种方式调用 Python 代码(如果它们调用你使用 Python 实现覆盖的 dunder 方法,这可能发生)或者如果你是使用dictand not a real的子类,dict或者如果他们的 C 实现释放 GIL。依赖它们是原子的并不是一个好主意。
您提到iter()如果其底层可迭代更改大小,则会出错,但这在这里无关紧要,因为.keys(),.values()并.items()返回一个视图对象,并且底层对象更改没有问题:
D.keys()
如果您一次在多个指令中修改 dict,您有时会d处于不一致的状态,其中一些修改已经完成,有些还没有,但您不应该得到RuntimeError像您那样的修改with iter(), 除非你以非原子的方式修改它。