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

特别是在 Python 中,如何在线程之间共享变量?

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

关于使用锁对线程间共享数据进行同步访问,我已经看到过许多警告,但是我还没有看到这个问题的一个真正好的例子。

先谢谢你!

72303 次浏览

在 Python 中,除了 function-local 变量之外,所有内容都是共享的(因为每个函数调用都有自己的一组局部变量,而线程总是单独的函数调用)即使这样,也只有变量本身(引用对象的名称)是函数本地的; 对象本身总是全局的,任何东西都可以引用它们。 在这方面,特定线程的 Thread对象不是特殊对象。如果将 Thread对象存储在所有线程都可以访问的地方(如全局变量) ,那么所有线程都可以访问该 Thread对象。如果要自动修改另一个线程可以访问的 什么都行,则必须使用锁来保护它。当然,所有线程都必须共享这个非常相同的锁,否则它不会非常有效。

如果需要实际的线程本地存储,那就需要使用 threading.localthreading.local的属性不在线程之间共享; 每个线程只看到它自己放在其中的属性。如果您对它的实现感到好奇,那么源代码在标准库的 _ threading _ local. py中。

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

与 Python 的一个不同之处在于,这种 GIL 意味着一次只能有一个线程运行 Python 代码。然而,当涉及到同步访问时,这并没有多大帮助,因为所有常见的优先权问题仍然适用,并且您必须像在其他语言中一样使用线程原语。但是,这确实意味着您需要重新考虑是否使用线程来提高性能。

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

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

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

考虑以下代码:

#/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()
I'm called from Thread-2
I'm called from Thread-1 

在这里,使用 threading.local ()作为一种快速而肮脏的方法,在不更改 foo ()接口的情况下将一些数据从 run ()传递到 bar ()。

请注意,使用全局变量不会奏效:

#/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()
I'm called from Thread-2
I'm called from 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())

但是,当使用第三方或设计糟糕的代码时,这并不总是可能的。

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

这句话看起来有点不对: “如果您想要自动修改其他线程可以访问的任何内容,那么您必须使用一个锁来保护它。”我认为这种说法是有效的,但并不完全准确。我认为“原子”这个术语意味着 Python 解释器创建了一个字节码块,没有给 CPU 留下任何中断信号的空间。

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

像“ thread _ running [5] = True”这样的 Python 代码不是原子的。这里有两个 Python 字节代码块; 一个用于为对象解引用 list () ,另一个字节代码块用于为对象赋值,在本例中为列表中的“位置”。可以在 <-两个字节码-> 块 <-之间引发中断-> 。那是发生了不好的事情。

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

值得一提的是,threading.local()不是单例模式。

您可以在每个线程中使用更多它们。 是 一个仓库都没有