Python中的线程本地存储


76

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

有关


1
我不确定您要问什么-记录了threading.local,并且您或多或少地粘贴了以下文档……
Glenn Maynard 2009年

2
@Glenn我将文档粘贴到了我的答案之一中。我在另一个引用了Alex的解决方案。我只是使这些内容更易于访问。
Casebash

想象一下,批评有帮助的志愿者将关键文档重新格式化为可移动访问的StackOverflow答案,该答案以前只能通过在交互式CLI REPL中手动键入混淆的Python语句(例如import _threading_local as tl\nhelp(tl))来读取。</yikes>
塞西尔·库里

Answers:


129

例如,如果您有一个线程工作池,并且每个线程都需要访问其自己的资源(例如网络或数据库连接),则线程本地存储很有用。请注意,该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以及每个线程ID和每个命令的计数会因运行而异。


9
“请注意,线程模块使用常规的线程概念(可以访问进程全局数据),但是由于全局解释器锁,它们并不太有用。”这是严重的吗?如果我正确地阅读了这篇文章,这将极具误导性,因为线程非常有用且至关重要,无论是否使用GIL。
zzzeek 2015年

2
wont_work函数是错误的,但不是因为threading.local“必须在全局范围内使用”。而是,代码使用局部变量(threading.local对象),并期望其在调用之间保留值。这不是局部变量的行为方式(如果使用简单的字典,就会遇到相同的问题)。
Paul Moore

1
@zehelvion它们对于同时运行多个功能很有用。
zzzeek

@zzzeek但是Python中的进程做同样的事情吗?不,除了共享相同的全局变量和具有唯一的全局变量之外,还有什么区别?
AturSams,2015年

4
您能以粗体显示吗:“一个容易被忽略的重要事项:一个threading.local()对象只需要创建一次,而不是每个线程一次,也不是每个函数调用一次” :)-我以为我疯了!
城市

25

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

就像普通对象一样,您可以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属性是类成员(并因此在线程之间共享)也是如此。

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


具有class属性的全局类-有趣;我将看看是否还能解决我遇到的问题。
伊桑·弗曼

另一方面,在程序启动时初始化一次的简单全局对象通常是最简单的解决方案,这是事实。并非要这样做-就像任何变量一样,它取决于应用程序。
保罗·摩尔

我现在在专业地使用Python的地方,已经很长时间没有这样做了。但是,由于ns是类成员,我们不应该将其用作Worker.ns吗?我知道,目前的代码工作,因为self.ns,作为一个getter,给出了相同的结果Worker.ns,但作为最佳做法显得扑朔迷离(在某些情况下可能是容易出错-这样做self.ns = ...不会 修改类的成员,但创建新实例级别字段)。你怎么看?
guyarad

self我想,使用类还是很大程度上取决于样式。使用的优点self是它可以与子类一起使用,而硬编码类名不会。OTOH,它的缺点是,您可能会不小心用实例变量遮盖了类变量。
Paul Moore

17

正如问题中指出的那样,亚历克斯·马特利(Alex Martelli)在此提供了一个解决方案。此函数使我们可以使用工厂函数为每个线程生成默认值。

#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

1
如果您正在执行此操作,则您真正想要的可能是defaultdict + ThreadLocalDict,但我认为没有此功能的通用实现。(defaultdict实际上应该是dict的一部分,例如dict(default=int),它将消除对“ ThreadLocalDefaultDict”的需要。)
Glenn Maynard

1
@Glenn,问题dict(default=int)在于dict()构造函数接受kwarg并将它们添加到dict。因此,如果实现了该功能,人们将无法指定称为“默认”的键。但是我实际上认为这对于您所展示的实现来说是一个很小的代价。毕竟,还有其他方法可以为字典添加键。
Evan Fosmark 09年

@Evan-我同意这种设计会更好,但是会破坏向后兼容性
Casebash

1
@Glenn,如果您的意思是这样,那么我将这种方法用于很多ARE N'T defaultdict的线程局部变量。如果您的意思是该接口与defaultdict应具有的接口类似(为工厂函数提供可选的位置和命名args:每次您可以存储回调时,都应该可以为其传递args!-),那么,sorta,除了我通常对不同的变量名使用不同的factory-and-args,而且我提供的方法在Python 2.4上也可以正常工作(不要问...!-)。
亚历克斯·马丁里

@Casebash:调用不threadlocal = threading.local()应该threadlocal_var()函数内部,这样它才能获取正在调用它的线程的本地地址吗?
martineau

5

也可以写

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

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


4
与其将这样的代码放在自己的答案中,不如仅仅编辑您的问题?
Evan Fosmark 09年

3
@Evan:因为有两种基本方法,它们实际上是分别的答案
Casebash 2010年

3

在模块/文件之间进行线程本地存储的方式。以下内容已在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中,我访问当前线程的线程局部变量。

此外,只需打印'dictionary'变量,这样您就可以看到可用的默认值,例如kwargs,args等

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.