首页 > 解决方案 > 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。有消息称该操作不是原子操作,请参见此处。但是,我无法让第一个示例崩溃。

我知道在这里做的安全的事情是使用一些锁而不是试图依赖某些操作的原子性。但是,我正在使用类似代码的第​​三方库中调试一个问题,并且我不一定要直接更改。

标签: pythonpython-3.x

解决方案


简短的回答:可能没问题,但无论如何都要使用锁。

使用dis你可以看到这list(d.items())实际上是两个字节码指令(68):

>>> 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 中实现并在 Cd.items()实现,因此每个都应该是原子的,除非它们最终以某种方式调用 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)]

如果您一次在多个指令中修改 dict,有时您会d处于不一致的状态,其中一些修改已经完成,有些还没有,但您不应该RuntimeError像您一样with iter(), 除非你以非原子的方式修改它。


推荐阅读