什么是 Python 中的“线程本地存储”,为什么需要它?

Posted

技术标签:

【中文标题】什么是 Python 中的“线程本地存储”,为什么需要它?【英文标题】:What is "thread local storage" in Python, and why do I need it? 【发布时间】:2010-09-11 10:10:21 【问题描述】:

具体来说,在 Python 中,变量如何在线程之间共享?

虽然我之前使用过threading.Thread,但我从未真正理解或看到变量如何共享的示例。它们是在主线程和子线程之间共享还是仅在子线程之间共享?我什么时候需要使用线程本地存储来避免这种共享?

我已经看到很多关于使用锁在线程之间同步访问共享数据的警告,但我还没有看到一个很好的问题示例。

提前致谢!

【问题讨论】:

标题与问题不符。问题是关于线程之间共享变量,标题暗示它专门关于线程本地存储 @Casebash:从这个问题的声音中,Mike 读到 TLS 是避免共享数据引起的问题所必需的,但不清楚默认共享哪些数据,与哪些数据共享,以及它是如何共享的。我调整了标题以更好地匹配问题。 【参考方案1】:

值得一提的是threading.local() 不是单身人士。

您可以在每个线程中使用更多它们。 它不是一个存储

【讨论】:

【参考方案2】:

我在这里可能错了。如果您知道其他情况,请详细说明,因为这将有助于解释为什么需要使用线程 local()。

这句话似乎不对,没有错:“如果你想原子地修改另一个线程可以访问的任何东西,你必须用锁来保护它。”我认为这句话 -> 有效

我认为原子操作是不能访问中断的 Python 字节码块。像“running = True”这样的 Python 语句是原子的。在这种情况下,您不需要锁定 CPU 以防止中断(我相信)。 Python 字节码分解不会被线程中断。

像“threads_running[5] = True”这样的 Python 代码不是原子的。这里有两块 Python 字节码;一个用于取消引用对象的 list() 和另一个字节码块以将值分配给对象,在这种情况下是列表中的“位置”。可以引发中断-->在块

线程 local() 与“原子”有何关系?这就是为什么该声明似乎误导了我。如果不是,你能解释一下吗?

【讨论】:

【参考方案3】:

在 Python 中,所有东西都是共享的,除了函数局部变量(因为每个函数调用都有自己的一组局部变量,并且线程总是单独的函数调用。)即使那样,只有变量本身(引用的名称to objects) 是函数的局部变量;对象本身总是全局的,任何东西都可以引用它们。 特定线程的Thread 对象在这方面不是特殊对象。如果您将Thread 对象存储在所有线程都可以访问的位置(如全局变量),那么所有线程都可以访问该Thread 对象。如果你想原子地修改另一个线程可以访问的任何东西,你必须用锁来保护它。当然,所有线程都必须共享这个相同的锁,否则它不会很有效。

如果你想要实际的线程本地存储,那就是threading.local 的用武之地。threading.local 的属性在线程之间不共享;每个线程只看到它自己放置在那里的属性。如果你对它的实现感到好奇,源代码在标准库中的_threading_local.py。

【讨论】:

您能否详细说明以下句子? “如果你想原子地修改任何你不是在同一个线程中创建的东西,并且没有存储在任何其他线程可以访问的地方,你必须用锁来保护它。” @changyuheng:这里解释一下什么是原子动作:cs.nott.ac.uk/~psznza/G52CON/lecture4.pdf @TomBusby:如果没有其他线程可以访问它,为什么我们需要用锁来保护它,即为什么我们需要使进程原子化? 请你举个简单的例子:“对象本身总是全局的,任何东西都可以引用它们”。通过引用假设您的意思是读取而不是分配/附加? @variable:我认为他的意思是values have no scope【参考方案4】:

考虑以下代码:

#/usr/bin/env python

from time import sleep
from random import random
from threading import Thread, local

data = local()

def bar():
    print("I'm called from", data.v)

def foo():
    bar()

class T(Thread):
    def run(self):
        sleep(random())
        data.v = self.getName()   # Thread-1 and Thread-2 accordingly
        sleep(1)
        foo()
 >> T().start(); T().start()
我是从 Thread-2 调用的
我是从 Thread-1 调用的 

这里 threading.local() 被用作将一些数据从 run() 传递到 bar() 而不更改 foo() 接口的快速而肮脏的方法。

请注意,使用全局变量并不能解决问题:

#/usr/bin/env python

from time import sleep
from random import random
from threading import Thread

def bar():
    global v
    print("I'm called from", v)

def foo():
    bar()

class T(Thread):
    def run(self):
        global v
        sleep(random())
        v = self.getName()   # Thread-1 and Thread-2 accordingly
        sleep(1)
        foo()
 >> T().start(); T().start()
我是从 Thread-2 调用的
我是从 Thread-2 调用的 

同时,如果您能负担得起将这些数据作为 foo() 的参数传递 - 这将是一种更优雅且设计良好的方式:

from threading import Thread

def bar(v):
    print("I'm called from", v)

def foo(v):
    bar(v)

class T(Thread):
    def run(self):
        foo(self.getName())

但这在使用第三方或设计不佳的代码时并不总是可行的。

【讨论】:

【参考方案5】:

您可以使用threading.local() 创建线程本地存储。

>>> tls = threading.local()
>>> tls.x = 4 
>>> tls.x
4

存储到 tls 的数据对于每个线程都是唯一的,这将有助于确保不会发生意外共享。

【讨论】:

【参考方案6】:

就像在其他所有语言中一样,Python 中的每个线程都可以访问相同的变量。 “主线程”和子线程之间没有区别。

与 Python 的一个区别是全局解释器锁意味着一次只能有一个线程运行 Python 代码。然而,在同步访问方面,这并没有多大帮助,因为所有常见的抢占问题仍然适用,并且您必须像在其他语言中一样使用线程原语。但是,这确实意味着您需要重新考虑是否使用线程来提高性能。

【讨论】:

以上是关于什么是 Python 中的“线程本地存储”,为什么需要它?的主要内容,如果未能解决你的问题,请参考以下文章

线程本地存储

ThreadLocal变量

线程本地存储进程

线程本地存储,thread_local关键字是必须的吗?

Python中ThreadLocal的理解与使用

深入理解ThreadLocal