【问题标题】:Thread local storage in PythonPython中的线程本地存储
【发布时间】:2010-11-27 08:21:20
【问题描述】:

如何在 Python 中使用线程本地存储?

相关

【问题讨论】:

  • 我不确定你在问什么——threading.local 已记录在案,而且你或多或少粘贴了以下文档...
  • @Glenn 我将文档粘贴到了我的一个中。我在另一个中引用了亚历克斯的解决方案。我只是让这些内容更易于访问。
  • 想象一下,通过在交互式 CLI REPL 中手动输入模糊的 Python 语句(例如,import _threading_local as tl\nhelp(tl) )。 </yikes>

标签: python multithreading thread-local-storage


【解决方案1】:

我做线程本地存储的方式跨模块/文件。以下已在 Python 3.5 中测试过 -

import threading
from threading import current_thread

# fileA.py 
def functionOne:
    thread = Thread(target = fileB.functionTwo)
    thread.start()

#fileB.py
def functionTwo():
    currentThread = threading.current_thread()
    dictionary = currentThread.__dict__
    dictionary["localVar1"] = "store here"   #Thread local Storage
    fileC.function3()

#fileC.py
def function3():
    currentThread = threading.current_thread()
    dictionary = currentThread.__dict__
    print (dictionary["localVar1"])           #Access thread local Storage

在 fileA 中,我启动了一个线程,该线程在另一个模块/文件中具有目标函数。

在 fileB 中,我在该线程中设置了一个我想要的局部变量。

在fileC中,我访问当前线程的线程局部变量。

此外,只需打印“字典”变量,以便您可以看到可用的默认值,如 kwargs、args 等

【讨论】:

    【解决方案2】:

    线程本地存储很有用,例如,如果您有一个线程工作池并且每个线程都需要访问自己的资源,例如网络或数据库连接。请注意,threading 模块使用线程的常规概念(可以访问进程全局数据),但由于全局解释器锁定,这些并不太有用。不同的multiprocessing 模块为每个模块创建一个新的子进程,因此任何全局都将是线程本地的。

    线程模块

    这是一个简单的例子:

    import threading
    from threading import current_thread
    
    threadLocal = threading.local()
    
    def hi():
        initialized = getattr(threadLocal, 'initialized', None)
        if initialized is None:
            print("Nice to meet you", current_thread().name)
            threadLocal.initialized = True
        else:
            print("Welcome back", current_thread().name)
    
    hi(); hi()
    

    这将打印出来:

    Nice to meet you MainThread
    Welcome back MainThread
    

    一件很容易被忽视的重要事情:threading.local() 对象只需要创建一次,而不是每个线程一次,也不是每个函数调用一次。 globalclass 级别是理想的位置。

    原因如下:threading.local() 实际上在每次调用时都会创建一个新实例(就像任何工厂或类调用一样),因此多次调用 threading.local() 会不断覆盖原始对象,这很可能不是想要什么。当任何线程访问现有的threadLocal 变量(或任何它被调用的变量)时,它都会获得该变量的私有视图。

    这不会按预期工作:

    import threading
    from threading import current_thread
    
    def wont_work():
        threadLocal = threading.local() #oops, this creates a new dict each time!
        initialized = getattr(threadLocal, 'initialized', None)
        if initialized is None:
            print("First time for", current_thread().name)
            threadLocal.initialized = True
        else:
            print("Welcome back", current_thread().name)
    
    wont_work(); wont_work()
    

    将产生以下输出:

    First time for MainThread
    First time for MainThread
    

    多处理模块

    所有全局变量都是线程本地的,因为multiprocessing 模块为每个线程创建一个新进程。

    考虑这个例子,processed 计数器是线程本地存储的一个例子:

    from multiprocessing import Pool
    from random import random
    from time import sleep
    import os
    
    processed=0
    
    def f(x):
        sleep(random())
        global processed
        processed += 1
        print("Processed by %s: %s" % (os.getpid(), processed))
        return x*x
    
    if __name__ == '__main__':
        pool = Pool(processes=4)
        print(pool.map(f, range(10)))
    

    它会输出如下内容:

    Processed by 7636: 1
    Processed by 9144: 1
    Processed by 5252: 1
    Processed by 7636: 2
    Processed by 6248: 1
    Processed by 5252: 2
    Processed by 6248: 2
    Processed by 9144: 2
    Processed by 7636: 3
    Processed by 5252: 3
    [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
    

    ...当然,线程 ID 和每个线程的计数和顺序会因运行而异。

    【讨论】:

    • "注意线程模块使用线程的常规概念(可以访问进程全局数据),但是由于全局解释器锁,这些并没有太大用处。" 这严重吗?如果我没看错的话,这将非常具有误导性,因为线程非常有用且至关重要,无论 GIL 与否。
    • wont_work 函数是错误的,但不是因为 threading.local “必须在全局范围内使用”。相反,代码使用局部变量(threading.local 对象)并期望它在调用之间保留值。这不是局部变量的行为方式(使用普通 dict 也会遇到同样的问题)。
    • @zehelvion 它们对于同时运行多个功能很有用。
    • @zzzeek 但是Python中的进程做同样的事情吗?不,除了共享相同的全局变量和拥有唯一的全局变量之外,还有什么区别?
    • 您能否用粗体表示:“一件容易被忽视的重要事情:threading.local() 对象只需要创建一次,而不是每个线程一次,也不是每个函数调用一次”:) - 我以为我疯了!
    【解决方案3】:

    线程本地存储可以简单地被认为是一个命名空间(通过属性符号访问值)。不同之处在于每个线程透明地获取自己的一组属性/值,因此一个线程看不到来自另一个线程的值。

    就像普通对象一样,您可以在代码中创建多个threading.local 实例。它们可以是局部变量、类或实例成员或全局变量。每一个都是一个单独的命名空间。

    这是一个简单的例子:

    import threading
    
    class Worker(threading.Thread):
        ns = threading.local()
        def run(self):
            self.ns.val = 0
            for i in range(5):
                self.ns.val += 1
                print("Thread:", self.name, "value:", self.ns.val)
    
    w1 = Worker()
    w2 = Worker()
    w1.start()
    w2.start()
    w1.join()
    w2.join()
    

    输出:

    Thread: Thread-1 value: 1
    Thread: Thread-2 value: 1
    Thread: Thread-1 value: 2
    Thread: Thread-2 value: 2
    Thread: Thread-1 value: 3
    Thread: Thread-2 value: 3
    Thread: Thread-1 value: 4
    Thread: Thread-2 value: 4
    Thread: Thread-1 value: 5
    Thread: Thread-2 value: 5
    

    请注意每个线程如何维护自己的计数器,即使 ns 属性是类成员(因此在线程之间共享)。

    同样的例子可以使用实例变量或局部变量,但这不会显示太多,因为那时没有共享(字典也可以工作)。在某些情况下,您需要将线程局部存储作为实例变量或局部变量,但它们往往相对较少(而且非常微妙)。

    【讨论】:

    • 具有类属性的全局类——有趣;我会看看这是否也解决了我遇到的问题。
    • 另一方面,在程序启动时初始化一次的简单全局对象确实是最简单的解决方案。并非您需要这样做 - 就像任何变量一样,它取决于应用程序。
    • 我现在专业使用 Python 的地方,很久没用了。但是,既然ns 是一个类成员,我们不应该把它当作Worker.ns 使用吗?我知道当前代码有效,因为 self.ns 作为吸气剂,给出与 Worker.ns 相同的结果,但作为一种似乎令人困惑的最佳实践(在某些情况下可能容易出错 - 执行 self.ns = ...修改类成员,而是创建一个新的实例级字段)。你怎么看?
    • 我猜,使用类或self 在很大程度上是风格问题。使用self 的优点是它可以与子类一起使用,而对类名进行硬编码则不会。 OTOH,它的缺点是可能会意外地用实例变量隐藏类变量,正如你所说的。
    【解决方案4】:

    正如问题中所述,Alex Martelli 给出了解决方案here。这个函数允许我们使用工厂函数为每个线程生成一个默认值。

    #Code originally posted by Alex Martelli
    #Modified to use standard Python variable name conventions
    import threading
    threadlocal = threading.local()    
    
    def threadlocal_var(varname, factory, *args, **kwargs):
      v = getattr(threadlocal, varname, None)
      if v is None:
        v = factory(*args, **kwargs)
        setattr(threadlocal, varname, v)
      return v
    

    【讨论】:

    • 如果你这样做,你真正想要的可能是 defaultdict + ThreadLocalDict,但我认为没有这个的股票实现。 (defaultdict 应该是 dict 的一部分,例如dict(default=int),这将消除对“ThreadLocalDefaultDict”的需要。)
    • @Glenn,dict(default=int) 的问题在于 dict() 构造函数接受 kwargs 并将它们添加到字典中。因此,如果实现了这一点,人们将无法指定一个名为“默认”的键。但我实际上认为这对于像你展示的那样的实现来说是一个很小的代价。毕竟,还有其他方法可以为字典添加键。
    • @Evan - 我同意这种设计会更好,但它会破坏向后兼容性
    • @Glenn,如果这就是你的意思,我将这种方法用于大量 AREN'T defaultdicts 的线程局部变量。如果您的意思是它具有与defaultdict 应该具有的类似接口(为工厂函数提供可选的位置和命名参数:每次您可以存储回调时,您都应该能够有选择地为其传递参数!-),那么,排序,除了我通常对不同的变量名使用不同的工厂和参数,而且我给出的方法在 Python 2.4 上也可以正常工作(不要问......!-)。
    • @Casebash: 调用 threadlocal = threading.local() 不应该在 inside threadlocal_var() 函数中以便获取调用它的线程的本地吗?
    【解决方案5】:

    也可以写

    import threading
    mydata = threading.local()
    mydata.x = 1
    

    mydata.x 将只存在于当前线程中

    【讨论】:

    • 与其将这类代码放在自己的答案中,不如直接编辑您的问题?
    • @Evan:因为有两种基本方法,它们确实是不同的答案
    猜你喜欢
    • 1970-01-01
    • 2023-04-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-04
    相关资源
    最近更新 更多