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

Ale*_*lex 34 python multithreading

访问/更改字典值是否是线程安全的?

我有一个全球性的词典foo和多线程与IDS id1,id2,... idn.foo如果已知每个线程只能使用其与id相关的值,那么访问和更改值是否可以不为它分配锁定,比如说线程id1只能使用foo[id1]吗?

Dir*_*irk 52

假设CPython:是的,不是.从多个并发读/写请求不会破坏字典的意义上来说,从共享字典中获取/存储值实际上是安全的.这是由于实现维护的全局解释器锁("GIL").那是:

线程A运行:

a = global_dict["foo"]
Run Code Online (Sandbox Code Playgroud)

线程B运行:

global_dict["bar"] = "hello"
Run Code Online (Sandbox Code Playgroud)

线程C运行:

global_dict["baz"] = "world"
Run Code Online (Sandbox Code Playgroud)

即使所有三次访问尝试都在"相同"时间发生,也不会破坏字典.解释器将以某种未定义的方式序列化它们.

但是,以下序列的结果未定义:

线程A:

if "foo" not in global_dict:
   global_dict["foo"] = 1
Run Code Online (Sandbox Code Playgroud)

线程B:

global_dict["foo"] = 2
Run Code Online (Sandbox Code Playgroud)

因为线程A中的测试/设置不是原子的("检查时间/使用时间"竞争条件).所以,如果你锁定东西通常是最好的:

from threading import RLock

lock = RLock()

def thread_A():
    lock.acquire()
    try:
        if "foo" not in global_dict:
            global_dict["foo"] = 1
    finally:
        lock.release()

def thread_B():
    lock.acquire()
    try:
        global_dict["foo"] = 2
    finally:
        lock.release()
Run Code Online (Sandbox Code Playgroud)

  • @Claudiu:如果密钥完全由 C 实现的内置函数组成,那么 `setdefault` 将在 CPython 中自动初始化。只要操作的变异部分发生,并且在开始变异和完成之间没有字节代码,GIL 就会保护您免受竞争的影响它,并且在键插入的情况下,当对象的 __eq__ 和 __hash__ 是用 C 而不是 Python 级别的代码实现时,您会得到这种行为。 (2认同)

Ale*_*lli 25

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

import threading
tloc = threading.local()
Run Code Online (Sandbox Code Playgroud)

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

线程的线程本地存储在线程结束时消失; 让线程记录它们的最终结果,put在它们终止之前将它们的结果放入一个公共实例Queue.Queue(这本质上是线程安全的).类似地,线程要处理的数据的初始值可以是在线程启动时传递的参数,也可以是从线程获取的参数Queue.

其他一些半生不熟的方法,例如希望看起来像原子的操作确实是原子的,可能适用于给定版本和Python版本中的特定情况,但很容易被升级或端口破坏.当一个合适,干净,安全的架构易于安排,便携,方便和快速时,没有真正的理由冒这些问题.

  • 对于像这样的简单情况,线程本地存储既是极端的杀伤力,又会带来不小的复杂性(例如,由于需要重新组合线程本地结果)。正如明智的答案所建议的,只需: **(A)** 全局声明 `dict_lock = threading.Lock()` 或 `dict_lock = threading.RLock()` 和 **(B)** 将每个字典访问包装在一个 `with dict_lock:` 上下文管理器。 (2认同)

yot*_*ota 17

因为我需要类似的东西,所以我降落在这里.我在这个简短的片段中总结了你的答案:

#!/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)
Run Code Online (Sandbox Code Playgroud)

因此,您可以使用with构造来保持锁定,同时摆弄你的dict()

  • **模糊的样板。**理想情况下,标记为“ThreadSafeDict”的类应该是一个“隐式”线程安全字典。这不是;它只是“threading.Lock”的一个毫无意义的薄包装。调用者仍然必须在显式上下文管理器中手动包装每个字典操作 - 这正是调用者使用直接“threading.Lock”所做的事情。奥奥 (7认同)

gim*_*mel 5

GIL需要的是关心,如果你碰巧使用CPython

全局解释器锁

Python线程用来确保一次仅一个线程在CPython虚拟机中执行的锁。确保没有两个进程可以同时访问相同的内存,从而简化了CPython的实现。锁定整个解释器可以使解释器更容易成为多线程的,但会牺牲多处理器机器提供的许多并行性。过去,人们一直在努力创建“自由线程”解释器(该解释器以更精细的粒度锁定共享数据),但是到目前为止,都没有成功,因为在普通的单处理器情况下性能会受到影响。

请参阅因为gil而在多线程python代码中不需要锁

  • 这并不意味着您可以依赖 GIL。关键可能是一个带有 `__hash__` 方法的类的实例,因此执行超过 1 个 Python 字节码指令并且线程可以*无论如何*切换。然后是 I/O 操作和释放 GIL 的本机代码部分。锁仍然是线程安全代码的必要条件。 (2认同)