在Python中使用线程的全局字典

72 投票
5 回答
70083 浏览
提问于 2025-04-15 13:46

访问或修改字典中的值是线程安全的吗?

我有一个全局的字典 foo,还有多个线程,分别有不同的ID,比如 id1id2,一直到 idn。如果我知道每个线程只会处理与自己ID相关的值,比如线程 id1 只会处理 foo[id1],那么我在访问和修改 foo 的值时,是否可以不加锁呢?

5 个回答

27

因为我需要类似的东西,所以我来到了这里。我把你们的回答总结成了这个简短的代码片段:

#!/usr/bin/env python3

import threading

class ThreadSafeDict(dict) :
    def __init__(self, * p_arg, ** n_arg) :
        dict.__init__(self, * p_arg, ** n_arg)
        self._lock = threading.Lock()

    def __enter__(self) :
        self._lock.acquire()
        return self

    def __exit__(self, type, value, traceback) :
        self._lock.release()

if __name__ == '__main__' :

    u = ThreadSafeDict()
    with u as m :
        m[1] = 'foo'
    print(u)

这样,你可以使用 with 这个结构来在操作你的 dict() 时保持锁定状态。

30

让每个线程使用独立数据的最佳、安全、便携的方法是:

import threading
tloc = threading.local()

现在,每个线程都在使用一个完全独立的 tloc 对象,尽管它是一个全局名称。线程可以在 tloc 上获取和设置属性,如果需要字典,可以使用 tloc.__dict__ 等等。

线程的本地存储在线程结束时会消失;为了让线程记录它们的最终结果,可以在它们结束之前,把结果 put 到一个公共的 Queue.Queue 实例中(这个是本质上线程安全的)。同样,线程要处理的数据的初始值可以在启动线程时作为参数传递,或者从一个 Queue 中获取。

其他不成熟的方法,比如希望看起来是原子操作的操作确实是原子操作,可能在某个特定版本的Python中对特定情况有效,但在升级或移植时很容易就会出问题。既然有一种合适、干净、安全的架构这么容易安排,而且便携、方便、快速,就没有必要冒这样的风险。

85

假设我们在使用CPython:可以说是,也可以说不是。实际上,从一个共享字典中获取或存储值是安全的,因为多个线程同时进行读写请求不会导致字典损坏。这是因为实现中有一个叫做全局解释器锁(“GIL”)的机制。也就是说:

线程A在运行:

a = global_dict["foo"]

线程B在运行:

global_dict["bar"] = "hello"

线程C在运行:

global_dict["baz"] = "world"

即使这三个线程同时尝试访问字典,也不会导致字典损坏。解释器会以某种未定义的方式将它们串行处理。

但是,以下操作的结果是未定义的:

线程A:

if "foo" not in global_dict:
   global_dict["foo"] = 1

线程B:

global_dict["foo"] = 2

因为线程A中的测试/设置操作不是原子的(也就是存在“检查时机/使用时机”的竞争条件)。所以,通常最好是,如果你加锁

from threading import RLock

lock = RLock()

def thread_A():
    with lock:
        if "foo" not in global_dict:
            global_dict["foo"] = 1

def thread_B():
    with lock:
        global_dict["foo"] = 2

撰写回答