dict.update()是否线程安全?

4
我想知道dict.update()在Python中是否线程安全。我已经阅读了相关的问题,但没有一个完全回答了我的问题。
我的问题非常具体和简单。例如,我已经有一个本地字典d2。我只需要像下面展示的那样使用d2更新全局字典dd最初为空,并用不同的线程填充。每个线程中的d2可能与d具有重叠条目(不认为这很重要)。它是线程安全的吗?
import dis

def f(d):
    d2 = {1:2, 3:4}
    d.update(d2)

print(dis.dis(f))

字节码如下所示:

 10           0 LOAD_CONST               1 (2)
              2 LOAD_CONST               2 (4)
              4 LOAD_CONST               3 ((1, 3))
              6 BUILD_CONST_KEY_MAP      2
              8 STORE_FAST               1 (d2)

 11          10 LOAD_FAST                0 (d)
             12 LOAD_ATTR                0 (update)
             14 LOAD_FAST                1 (d2)
             16 CALL_FUNCTION            1
             18 POP_TOP
             20 LOAD_CONST               0 (None)
             22 RETURN_VALUE

看起来16 CALL_FUNCTION是更新字典的原子函数。那么它应该是线程安全的吗?


我认为不是。请参考相关问题 如何使内置容器(集合、字典、列表)线程安全? - martineau
1
“CALL_FUNCTION” 基本上只是一个记住返回位置的跳转指令。它与被调用函数的原子性无关。 - chepner
@chepner,什么是原子性调用的关键?人们说每个字节码指令都是原子性的。我从来没有完全理解这一点。 - Tim
线程切换仅发生在字节码边界处,因此任何需要超过一个字节码的操作都需要显式锁定才能保证原子性。单个字节码的执行是否原子取决于其实现方式。在您的示例中,除了(可能!)LOAD_ATTRCALL_FUNCTION之外,所有操作都是原子的。这里没有通用原则,只有详细的实现知识。更多信息请参见:https://dev59.com/ploT5IYBdhLWcg3wtBNz#38320815 - Tim Peters
CALL_FUNCTION 只是开始调用;您仍然需要考虑函数本身的字节码(或其实现)*。 - chepner
那么,如果被调用的函数是用C实现的,并且不使用其他Python代码,它应该是原子的,对吧?使用锁有主要的缺点吗? - Tim
2个回答

11
如果键是内置的可哈希类型的组合,则通常情况下,“是”的,.update() 是线程安全的。特别是对于您示例中的整数键,是的。
但通常情况下不是。在字典中查找键可以调用用户提供的__hash__()__eq__()方法中的任意用户定义的Python代码,并且它们可以执行任何操作——包括对所涉及的字典进行自己的变化。一旦实现调用Python代码,其他线程也可以运行,包括“可能”对d1 和/或 d2 进行变异的线程。
这并不是内置的可哈希类型(int、str、float、tuple等)的潜在问题——它们用于计算哈希码和决定相等性的实现是纯函数的(确定性和无副作用),不会释放GIL(全局解释器锁)。
这都是关于CPython(Python的C实现)。 在其他实现下,答案可能不同!语言参考手册在这方面保持沉默。

非常感谢您提供如此详细的答案!是的,我正在使用CPython,我的键是int/str类型。 - Tim

0
如果您可以使用外部库,可以尝试查看locked-dict
从他们的自述文件中可以了解到:

使用锁允许上下文管理线程安全和可变迭代。

例如,从他们的tests中可以看到:

pip install locked-dict

import locked_dict

expected = 0
d = locked_dict.LockedDict()
assert len(d) == expected
assert bool(d) is False
assert d is not True
assert hasattr(d, '_lock')

empty_d = {}
assert d == empty_d

plain_old_d = {999: 'plain old dict', 12345: 54321}
assert d != plain_old_d

with d as m:
    assert len(m) == expected
    assert bool(m) is False
    assert m is not True
    assert hasattr(m, '_lock')
    assert m != plain_old_d
    assert m == empty_d

    m[0] = ['foo']
    expected += 1
    assert len(m) == expected
    assert bool(m) is True
    assert m is not False
    assert m != plain_old_d
    assert m != empty_d

    m.clear()
    expected -= 1
    assert len(m) == expected
    assert bool(m) is False
    assert m is not True
    assert m != plain_old_d
    assert m == empty_d

请注意,此库已有3年历史,尽管对于您的使用情况仍可能相关


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接