Usando una variable global con un hilo

84

¿Cómo comparto una variable global con hilo?

Mi ejemplo de código de Python es:

from threading import Thread
import time
a = 0  #global variable

def thread1(threadname):
    #read variable "a" modify by thread 2

def thread2(threadname):
    while 1:
        a += 1
        time.sleep(1)

thread1 = Thread( target=thread1, args=("Thread-1", ) )
thread2 = Thread( target=thread2, args=("Thread-2", ) )

thread1.join()
thread2.join()

No sé cómo hacer que los dos hilos compartan una variable.

Mauro Midolo
fuente

Respuestas:

97

Solo necesita declarar acomo una entrada global thread2, para que no esté modificando una aque sea local para esa función.

def thread2(threadname):
    global a
    while True:
        a += 1
        time.sleep(1)

En thread1, no necesita hacer nada especial, siempre y cuando no intente modificar el valor de a(lo que crearía una variable local que sombree la global; úsela global asi es necesario)>

def thread1(threadname):
    #global a       # Optional if you treat a as read-only
    while a < 10:
        print a
chepner
fuente
47

En una función:

a += 1

será interpretado por el compilador como assign to a => Create local variable a, que no es lo que desea. Probablemente fallará con un a not initializederror ya que el (local) a de hecho no se ha inicializado:

>>> a = 1
>>> def f():
...     a += 1
... 
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f
UnboundLocalError: local variable 'a' referenced before assignment

Puede obtener lo que desea con la globalpalabra clave (muy mal vista y por buenas razones) , así:

>>> def f():
...     global a
...     a += 1
... 
>>> a
1
>>> f()
>>> a
2

Sin embargo, en general, debe evitar el uso de variables globales que se salen de control con mucha rapidez. Y esto es especialmente cierto para los programas multiproceso, donde no tiene ningún mecanismo de sincronización para thread1saber cuándo ase ha modificado. En resumen: los hilos son complicados y no puede esperar tener una comprensión intuitiva del orden en el que ocurren los eventos cuando dos (o más) subprocesos funcionan con el mismo valor. El lenguaje, compilador, SO, procesador ... TODOS pueden jugar un papel, y decidir modificar el orden de las operaciones por velocidad, practicidad o cualquier otra razón.

La forma correcta para este tipo de cosas es usar las herramientas de intercambio de Python ( bloqueos y amigos), o mejor, comunicar datos a través de una cola en lugar de compartirlos, por ejemplo, así:

from threading import Thread
from queue import Queue
import time

def thread1(threadname, q):
    #read variable "a" modify by thread 2
    while True:
        a = q.get()
        if a is None: return # Poison pill
        print a

def thread2(threadname, q):
    a = 0
    for _ in xrange(10):
        a += 1
        q.put(a)
        time.sleep(1)
    q.put(None) # Poison pill

queue = Queue()
thread1 = Thread( target=thread1, args=("Thread-1", queue) )
thread2 = Thread( target=thread2, args=("Thread-2", queue) )

thread1.start()
thread2.start()
thread1.join()
thread2.join()
val
fuente
Esto resuelve un gran problema. Y parece más o menos el enfoque correcto para abordarlo.
Abhidemon
Esta es la forma en que estoy usando para resolver el problema de sincronización.
Zhang LongQI
1
Tengo algunas preguntas Primero, si tengo varias variables para compartir entre hilos, ¿necesito una cola separada para cada variable? En segundo lugar, ¿por qué se sincronizan las colas del programa anterior? ¿No debería cada uno actuar como copia local en cada función?
Esto es viejo, pero respondo de todos modos. La cola en sí no está sincronizada, no más que la variable a. Es el comportamiento de bloqueo predeterminado de la cola lo que crea la sincronización. La declaración a = q.get()se bloqueará (esperará) hasta que esté disponible un valor a. La variable qes local: si le asigna un valor diferente, sucederá solo localmente. Pero la cola que se le asigna en el código es la que está definida en el hilo principal.
1
No siempre es necesario utilizar una cola para compartir información entre hilos. El ejemplo en la respuesta de Chepner está perfectamente bien. Además, una cola no siempre es la herramienta adecuada. Una cola es útil, por ejemplo, si desea bloquear hasta que el valor esté disponible. Es inútil si los dos hilos compiten en un recurso compartido. Finalmente, las variables globales no son peores en los hilos. De hecho, pueden ser más naturales. Por ejemplo, su hilo puede ser simplemente un bloque de código, digamos un bucle, que necesita su propio proceso. Por tanto, el ámbito local se crea artificialmente cuando coloca el bucle en una función.
5

Se debe considerar el uso de un candado, como threading.Lock. Consulte objetos de bloqueo para obtener más información.

La respuesta aceptada PUEDE imprimir 10 por hilo1, que no es lo que desea. Puede ejecutar el siguiente código para comprender el error más fácilmente.

def thread1(threadname):
    while True:
      if a % 2 and not a % 2:
          print "unreachable."

def thread2(threadname):
    global a
    while True:
        a += 1

El uso de un candado puede prohibir el cambio amientras lee más de una vez:

def thread1(threadname):
    while True:
      lock_a.acquire()
      if a % 2 and not a % 2:
          print "unreachable."
      lock_a.release()

def thread2(threadname):
    global a
    while True:
        lock_a.acquire()
        a += 1
        lock_a.release()

Si el hilo usa la variable durante mucho tiempo, es una buena opción adaptarla primero a una variable local.

Jason Pan
fuente
3

Muchas gracias Jason Pan por sugerir ese método. La instrucción if thread1 no es atómica, de modo que mientras se ejecuta esa instrucción, es posible que thread2 se inmiscuya en thread1, permitiendo que se alcance el código no accesible. He organizado ideas de las publicaciones anteriores en un programa de demostración completo (a continuación) que ejecuté con Python 2.7.

Con un análisis cuidadoso, estoy seguro de que podríamos obtener más información, pero por ahora creo que es importante demostrar lo que sucede cuando el comportamiento no atómico se encuentra con los hilos.

# ThreadTest01.py - Demonstrates that if non-atomic actions on
# global variables are protected, task can intrude on each other.
from threading import Thread
import time

# global variable
a = 0; NN = 100

def thread1(threadname):
    while True:
      if a % 2 and not a % 2:
          print("unreachable.")
    # end of thread1

def thread2(threadname):
    global a
    for _ in range(NN):
        a += 1
        time.sleep(0.1)
    # end of thread2

thread1 = Thread(target=thread1, args=("Thread1",))
thread2 = Thread(target=thread2, args=("Thread2",))

thread1.start()
thread2.start()

thread2.join()
# end of ThreadTest01.py

Como se predijo, al ejecutar el ejemplo, el código "inalcanzable" a veces se alcanza realmente, produciendo una salida.

Solo para agregar, cuando inserté un par de adquisición / liberación de bloqueo en thread1, encontré que la probabilidad de que se imprimiera el mensaje "inalcanzable" se redujo considerablemente. Para ver el mensaje, reduje el tiempo de reposo a 0,01 segundos y aumenté NN a 1000.

Con un par de adquisición / liberación de bloqueo en thread1, no esperaba ver el mensaje en absoluto, pero está ahí. Después de insertar un par de adquisición / liberación de bloqueo también en thread2, el mensaje ya no apareció. En hind signt, la declaración de incremento en thread2 probablemente tampoco sea atómica.

Krista M Hill
fuente
1
Necesita los bloqueos en ambos subprocesos porque son cooperativos, "bloqueos de aviso" (no "obligatorios"). Tiene razón en que la declaración de incremento no es atómica.
Darkonaut
1

Bueno, ejemplo de ejecución:

¡ADVERTENCIA! ¡NUNCA HAGA ESTO EN CASA / TRABAJO! Solo en el aula;)

Utilice semáforos, variables compartidas, etc. para evitar condiciones de prisa.

from threading import Thread
import time

a = 0  # global variable


def thread1(threadname):
    global a
    for k in range(100):
        print("{} {}".format(threadname, a))
        time.sleep(0.1)
        if k == 5:
            a += 100


def thread2(threadname):
    global a
    for k in range(10):
        a += 1
        time.sleep(0.2)


thread1 = Thread(target=thread1, args=("Thread-1",))
thread2 = Thread(target=thread2, args=("Thread-2",))

thread1.start()
thread2.start()

thread1.join()
thread2.join()

y la salida:

Thread-1 0
Thread-1 1
Thread-1 2
Thread-1 2
Thread-1 3
Thread-1 3
Thread-1 104
Thread-1 104
Thread-1 105
Thread-1 105
Thread-1 106
Thread-1 106
Thread-1 107
Thread-1 107
Thread-1 108
Thread-1 108
Thread-1 109
Thread-1 109
Thread-1 110
Thread-1 110
Thread-1 110
Thread-1 110
Thread-1 110
Thread-1 110
Thread-1 110
Thread-1 110

Si el momento fuera el correcto, la a += 100operación se omitiría:

El procesador se ejecuta en T a+100y obtiene 104. Pero se detiene y salta al siguiente hilo Aquí, En T + 1 se ejecuta a+1con el valor anterior de a a == 4,. Entonces calcula 5. Salta hacia atrás (en T + 2), subproceso 1 y escribe a=104en la memoria. Ahora, de vuelta en el hilo 2, el tiempo es T + 3 y se escribe a=5en la memoria. ¡Voila! La siguiente instrucción de impresión imprimirá 5 en lugar de 104.

Error MUY desagradable para ser reproducido y atrapado.

visoft
fuente
Considere también agregar una implementación correcta. Eso sería muy útil para aquellos que están aprendiendo a compartir datos entre hilos.
JS.
1
Agregado a la lista "todo" :)
visoft