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


100

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

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

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

提前致谢!


2
标题与问题不符。问题是与线程之间共享变量有关,标题暗示它专门与线程本地存储有关
Casebash

2
@Casebash:从这个问题的声音来看,Mike读到TLS对于避免由共享数据引起的问题是必要的,但是不清楚默认情况下共享了哪些数据,共享了哪些数据以及如何共享。我已经调整标题以更好地匹配该问题。
Shog9

Answers:


83

在Python中,所有内容都是共享的,但函数局部变量除外(因为每个函数调用都有自己的局部变量集,而线程始终是单独的函数调用。)即使这样,也只有变量本身(引用对象的名称)对于功能而言是本地的;对象本身始终是全局的,任何事物都可以引用它们。Thread在这方面,特定线程的对象不是特殊对象。如果将Thread对象存储在所有线程可以访问的位置(例如全局变量),则所有线程都可以访问该Thread对象。如果要原子地修改另一个线程可以访问的任何内容,则必须使用锁来保护它。当然,所有线程都必须共享此非常相同的锁,否则效果不是很好。

如果您需要实际的线程本地存储,那就在这里threading.localthreading.local线程之间不共享属性。每个线程仅看到其自身放置在其中的属性。如果您对它的实现感到好奇,在标准库的_threading_local.py中找到源。


1
您能否提供有关以下句子的更多详细信息?“如果要原子地修改您不只是在同一线程中创建的任何内容,并且不存储其他线程可以访问的任何内容,则必须通过锁来保护它。”
张裕衡

@changyuheng:这是什么是原子作用的解释:cs.nott.ac.uk/~psznza/G52CON/lecture4.pdf
Tom Busby

1
@TomBusby:如果没有其他线程可以使用它,为什么我们需要通过锁来保护它,即为什么我们需要使进程原子化?
changyuheng

2
请您举一个简短的例子:“对象本身总是全局的,任何东西都可以引用它们”。通过引用假设您是指已读,而不是分配/附加?
变量

@variable:我认为他表示值没有范围
user1071847

75

考虑以下代码:

#/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()。开始()
我是从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()。开始()
我是从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())

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


18

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

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

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


2

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

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


0

我可能在这里错了。如果您还不知道,请进行阐述,因为这将有助于解释为什么需要使用线程local()。

该语句似乎正确无误:“如果您想自动地修改另一个线程可以访问的任何内容,则必须使用锁来保护它。” 我认为这句话->有效地<-是正确的,但并不完全正确。我认为“原子”一词意味着Python解释器创建了一个字节代码块,该块不留空间给CPU发送中断信号。

我认为原子操作是不提供对中断访问权限的Python字节代码块。Python语句(例如“ running = True”)是原子的。在这种情况下,您无需从中断中锁定CPU(我相信)。Python字节代码故障可以防止线程中断。

诸如“ threads_running [5] = True”之类的Python代码不是原子的。这里有两个Python字节代码块;一个取消引用对象的list(),另一个字节码块为对象分配值,在这种情况下为列表中的“位置”。可以引发中断->在<-两个字节码之间-> chunks <-。那是坏事发生的地方。

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


1
这看起来像是一个答案,但据报告由于问题而被认为是有问题的。我会避免在答案中要求澄清。这就是评论的目的。
达曼(Dharman)
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.