¿Qué es el "almacenamiento local de subprocesos" en Python y por qué lo necesito?

100

En Python específicamente, ¿cómo se comparten las variables entre subprocesos?

Aunque lo he usado threading.Threadantes, nunca entendí ni vi ejemplos de cómo se compartían las variables. ¿Se comparten entre el hilo principal y los niños o solo entre los niños? ¿Cuándo debería usar el almacenamiento local de subprocesos para evitar este intercambio?

He visto muchas advertencias sobre la sincronización del acceso a datos compartidos entre subprocesos mediante el uso de bloqueos, pero todavía no he visto un buen ejemplo del problema.

¡Gracias por adelantado!

Miguel
fuente
2
El título no coincide con la pregunta. La pregunta tiene que ver con el intercambio de variables entre subprocesos, el título implica que se trata específicamente del almacenamiento local de subprocesos
Casebash
2
@Casebash: por el sonido de esta pregunta, Mike leyó que TLS es necesario para evitar los problemas causados ​​por los datos compartidos, pero no estaba claro qué datos se compartían de forma predeterminada, con qué se compartían y cómo se compartían. Ajusté el título para que coincida mejor con la pregunta.
Shog9

Respuestas:

83

En Python, todo se comparte, excepto las variables de función local (porque cada llamada de función obtiene su propio conjunto de locales y los hilos son siempre llamadas de función separadas). E incluso entonces, solo las variables en sí mismas (los nombres que se refieren a objetos) son locales a la función; los objetos mismos son siempre globales y cualquier cosa puede referirse a ellos. El Threadobjeto de un hilo en particular no es un objeto especial a este respecto. Si almacena el Threadobjeto en algún lugar al que puedan acceder todos los subprocesos (como una variable global), todos los subprocesos pueden acceder a ese Threadobjeto. Si desea modificar atómicamente cualquier cosa a la que tenga acceso otro hilo, debe protegerlo con un candado. Y, por supuesto, todos los hilos deben compartir este mismo bloqueo, o no sería muy efectivo.

Si desea un almacenamiento local de subprocesos real, ahí es donde threading.localentra en juego. Los atributos de threading.localno se comparten entre subprocesos; cada hilo ve solo los atributos que él mismo colocó allí. Si tiene curiosidad por su implementación, la fuente está en _threading_local.py en la biblioteca estándar.

Thomas Wouters
fuente
1
¿Puede dar más detalles sobre la siguiente oración, por favor? "Si desea modificar atómicamente cualquier cosa que no haya creado simplemente en este mismo hilo, y no haya almacenado en ningún otro lugar al que pueda acceder, debe protegerlo con un candado".
Changyuheng
@changyuheng: Aquí hay una explicación de qué son las acciones atómicas: cs.nott.ac.uk/~psznza/G52CON/lecture4.pdf
Tom Busby
1
@TomBusby: Si no hay otros hilos que puedan acceder a él, ¿por qué necesitamos protegerlo con un candado, es decir, por qué necesitamos hacer que el proceso sea atómico?
Changyuheng
2
Por favor, puede dar un ejemplo rápido de: "los objetos en sí mismos son siempre globales y cualquier cosa puede hacer referencia a ellos". Por referir asumir, te refieres a leer y no asignar / adjuntar.
variable
@variable: creo que quiere decir que los valores no tienen alcance
user1071847
75

Considere el siguiente 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 (). Inicio (); T (). Inicio ()
Me llaman desde Thread-2
Estoy llamado desde Thread-1 

Aquí threading.local () se usa como una forma rápida y sucia de pasar algunos datos de run () a bar () sin cambiar la interfaz de foo ().

Tenga en cuenta que el uso de variables globales no funcionará:

#/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 (). Inicio (); T (). Inicio ()
Me llaman desde Thread-2
Me llaman desde Thread-2 

Mientras tanto, si pudiera permitirse pasar estos datos como un argumento de foo (), sería una forma más elegante y bien diseñada:

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

Pero esto no siempre es posible cuando se usa código de terceros o mal diseñado.

ahatchkins
fuente
18

Puede crear almacenamiento local de subprocesos utilizando threading.local().

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

Los datos almacenados en el tls serán únicos para cada hilo, lo que ayudará a garantizar que no se comparta de forma no intencionada.

Aaron Maenpaa
fuente
2

Al igual que en cualquier otro idioma, cada hilo de Python tiene acceso a las mismas variables. No hay distinción entre el 'hilo principal' y los hilos secundarios.

Una diferencia con Python es que el bloqueo de intérprete global significa que solo un hilo puede ejecutar código Python a la vez. Sin embargo, esto no es de mucha ayuda cuando se trata de sincronizar el acceso, ya que todavía se aplican todos los problemas habituales de preferencia, y debe usar primitivas de subprocesamiento como en otros idiomas. Sin embargo, significa que debe reconsiderar si estaba utilizando subprocesos para el rendimiento.

Nick Johnson
fuente
0

Puede que me equivoque aquí. Si sabe lo contrario, explique, ya que esto ayudaría a explicar por qué se necesitaría usar el hilo local ().

Esta afirmación parece apagada, no incorrecta: "Si desea modificar atómicamente cualquier cosa a la que tenga acceso otro hilo, debe protegerlo con un candado". Creo que esta afirmación es -> efectivamente <- correcta pero no del todo precisa. Pensé que el término "atómico" significaba que el intérprete de Python creaba un fragmento de código de bytes que no dejaba espacio para una señal de interrupción a la CPU.

Pensé que las operaciones atómicas son fragmentos de código de bytes de Python que no dan acceso a las interrupciones. Las declaraciones de Python como "running = True" son atómicas. No es necesario bloquear la CPU de las interrupciones en este caso (creo). El desglose del código de bytes de Python está a salvo de la interrupción del hilo.

El código Python como "threads_running [5] = True" no es atómico. Aquí hay dos fragmentos de código de bytes de Python; uno para desvincular la lista () de un objeto y otro fragmento de código de bytes para asignar un valor a un objeto, en este caso, un "lugar" en una lista. Se puede generar una interrupción -> entre <- los dos códigos de bytes -> fragmentos <-. Eso es donde pasan cosas malas.

¿Cómo se relaciona el hilo local () con "atómico"? Es por eso que la declaración me parece mal dirigida. Si no, ¿puedes explicarlo?

DevPlayer
fuente
1
Esto parece una respuesta, pero supongo que se informó como problemático debido a las preguntas formuladas. Evitaría pedir aclaraciones en la respuesta. Para eso están los comentarios.
Dharman