O que é “thread local storage” em Python e por que eu preciso dele?

100

Especificamente em Python, como as variáveis ​​são compartilhadas entre os threads?

Embora eu tenha usado threading.Threadantes, nunca realmente entendi ou vi exemplos de como as variáveis ​​são compartilhadas. Eles são compartilhados entre o tópico principal e os filhos ou apenas entre os filhos? Quando eu precisaria usar o armazenamento local do thread para evitar esse compartilhamento?

Já vi muitos avisos sobre a sincronização de acesso a dados compartilhados entre threads usando bloqueios, mas ainda não vi um exemplo realmente bom do problema.

Desde já, obrigado!

Mike
fonte
2
O título não corresponde à pergunta. A questão é sobre o compartilhamento de variáveis ​​entre threads, o título implica que é especificamente sobre o armazenamento local de threads
Casebash
2
@Casebash: pelo som desta pergunta, Mike leu que o TLS é necessário para evitar os problemas causados ​​pelos dados compartilhados, mas não estava claro sobre quais dados eram compartilhados por padrão, com o que eram compartilhados e como foram compartilhados. Eu ajustei o título para melhor corresponder à pergunta.
Shog9

Respostas:

83

Em Python, tudo é compartilhado, exceto as variáveis ​​locais da função (porque cada chamada de função obtém seu próprio conjunto de locais e os threads são sempre chamadas de função separadas.) E mesmo assim, apenas as próprias variáveis ​​(os nomes que se referem aos objetos) são locais para a função; os próprios objetos são sempre globais e qualquer coisa pode se referir a eles. O Threadobjeto para um determinado segmento não é um objeto especial a esse respeito. Se você armazenar o Threadobjeto em algum lugar que todos os threads podem acessar (como uma variável global), então todos os threads podem acessar aquele Threadobjeto. Se você deseja modificar atomicamente qualquer coisa a que outro thread tenha acesso, você deve protegê-lo com um bloqueio. E todos os threads devem compartilhar exatamente esse mesmo bloqueio, ou não seria muito eficaz.

Se você deseja armazenamento local de thread real, é aí que threading.localentra. Os atributos de threading.localnão são compartilhados entre os threads; cada thread vê apenas os atributos que ela mesma colocou lá. Se você estiver curioso sobre sua implementação, a fonte está em _threading_local.py na biblioteca padrão.

Thomas Wouters
fonte
1
Você pode dar mais detalhes sobre a seguinte frase, por favor? "Se você deseja modificar atomicamente qualquer coisa que não criou apenas neste mesmo thread e não armazenou em nenhum lugar que outro thread possa acessá-lo, você deve protegê-lo com um cadeado."
changyuheng
@changyuheng: Aqui está uma explicação do que são ações atômicas: cs.nott.ac.uk/~psznza/G52CON/lecture4.pdf
Tom Busby
1
@TomBusby: Se não há nenhum outro encadeamento que possa acessá-lo, por que precisamos protegê-lo com um bloqueio, ou seja, por que precisamos tornar o processo atômico?
changyuheng
2
Por favor, você pode dar um exemplo rápido de: "os próprios objetos são sempre globais e qualquer coisa pode se referir a eles". Por referir, pressupõe que você quer dizer ler e não atribuir / anexar?
variável de
@variable: acho que ele quer dizer que os valores não têm escopo
user1071847
75

Considere o seguinte código:

#/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 ()
Sou chamado do Thread-2
Eu sou chamado do Tópico 1 

Aqui, threading.local () é usado como uma maneira rápida e suja de passar alguns dados de run () para bar () sem alterar a interface de foo ().

Observe que usar variáveis ​​globais não resolverá o problema:

#/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 ()
Sou chamado do Thread-2
Sou chamado do Thread-2 

Enquanto isso, se você pudesse passar esses dados como um argumento de foo (), seria uma maneira mais elegante e bem projetada:

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())

Mas isso nem sempre é possível ao usar código de terceiros ou mal projetado.

Ahatchkins
fonte
18

Você pode criar o armazenamento local do thread usando threading.local().

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

Os dados armazenados nos tls serão exclusivos para cada thread, o que ajudará a garantir que o compartilhamento não intencional não ocorra.

Aaron Maenpaa
fonte
2

Assim como em qualquer outra linguagem, cada thread em Python tem acesso às mesmas variáveis. Não há distinção entre o 'thread principal' e os threads filho.

Uma diferença com Python é que o Global Interpreter Lock significa que apenas um thread pode estar executando o código Python por vez. No entanto, isso não ajuda muito quando se trata de sincronizar o acesso, já que todos os problemas de preempção usuais ainda se aplicam e você precisa usar primitivas de threading como em outras linguagens. Isso significa que você precisa reconsiderar se estava usando threads para desempenho, no entanto.

Nick Johnson
fonte
0

Posso estar errado aqui. Se você souber de outra forma, explique, pois isso ajudaria a explicar por que seria necessário usar o thread local ().

Esta afirmação parece errada, não está errada: "Se você deseja modificar atomicamente qualquer coisa a que outro thread tenha acesso, você deve protegê-lo com um cadeado." Eu acho que esta afirmação é -> efetivamente <- correta, mas não totalmente correta. Achei que o termo "atômico" significava que o interpretador Python criava um fragmento de código de bytes que não deixava espaço para um sinal de interrupção para a CPU.

Achei que as operações atômicas são pedaços de código de bytes do Python que não dão acesso a interrupções. As instruções do Python como "running = True" são atômicas. Você não precisa bloquear a CPU de interrupções neste caso (eu acredito). A divisão do código de bytes do Python está protegida contra interrupção do thread.

O código Python como "threads_running [5] = True" não é atômico. Existem dois blocos de código de bytes Python aqui; um para remover a referência da lista () de um objeto e outro fragmento de código de byte para atribuir um valor a um objeto, neste caso um "lugar" em uma lista. Uma interrupção pode ser gerada -> entre <- os dois códigos de byte -> blocos <-. Isso é onde coisas ruins acontecem.

Como o thread local () se relaciona com "atômico"? É por isso que a declaração parece enganosa para mim. Se não, você pode explicar?

DevPlayer
fonte
1
Parece uma resposta, mas foi relatado como problemático, suponho que devido às perguntas feitas. Eu evitaria pedir esclarecimentos na resposta. É para isso que servem os comentários.
Dharman