2009-07-02 11 views
14

estaba leyendo this question (que no tiene que leer porque voy a copiar lo que está ahí ... Yo sólo quería dar espectáculo que mi inspiración) ...¿Está modificando una variable de clase en python threadsafe?

Por lo tanto, si tengo una clase que cuenta cuántos casos se han creado:

class Foo(object): 
    instance_count = 0 
    def __init__(self): 
    Foo.instance_count += 1 

Mi pregunta es, si puedo crear objetos Foo en varios subprocesos, se instance_count va a ser correcto? ¿Las variables de clase son seguras para modificar desde múltiples hilos?

Respuesta

21

No es seguro incluso en CPython. Prueba esto a ver por sí mismo:

import threading 

class Foo(object): 
    instance_count = 0 

def inc_by(n): 
    for i in xrange(n): 
     Foo.instance_count += 1 

threads = [threading.Thread(target=inc_by, args=(100000,)) for thread_nr in xrange(100)] 
for thread in threads: thread.start() 
for thread in threads: thread.join() 

print(Foo.instance_count) # Expected 10M for threadsafe ops, I get around 5M 

La razón es que, si bien es INPLACE_ADD atómica bajo GIL, el atributo está siendo cargado y tienda (ver dis.dis (Foo .__ init__)). Use un candado para serializar el acceso a la variable de clase:

Foo.lock = threading.Lock() 

def interlocked_inc(n): 
    for i in xrange(n): 
     with Foo.lock: 
      Foo.instance_count += 1 

threads = [threading.Thread(target=interlocked_inc, args=(100000,)) for thread_nr in xrange(100)] 
for thread in threads: thread.start() 
for thread in threads: thread.join() 

print(Foo.instance_count) 
+0

Creo que en tu segundo ejemplo quieres que el objetivo del hilo sea interbloqueado_inc en lugar de inc_by. – tgray

+0

Gracias, corregido. La programación de copiar y pegar demasiado liberal me alcanza a veces. –

+0

gracias hormigas Aasma :-). Esto es lo que sospechaba. Gracias por probarlo. Como señala tgray, tu segundo objetivo debe estar interbloqueado_inc. Pero una vez que cambias eso ... se ve impecable. – Tom

-4

Diría que es seguro para subprocesos, al menos en la implementación de CPython. GIL hará que todos sus "hilos" se ejecuten secuencialmente para que no puedan interferir con su recuento de referencias.

+2

¿Es Foo.instance_count + = 1 y unidad atómica de trabajo? –

+0

Quizás no entiendo cómo funciona el GIL ... pero todavía no lo veo. Can not Thread1 lee instance_count. Entonces thread1 se detiene. Thread2 lee instance_count, luego se detiene. Thread1 modifica y escribe. Thread2 escribe. ¿Entonces pierdes un incremento? ¿Cómo garantiza el GIL que el hilo se ejecuta en toda la operación + =? – Tom

+0

Ja, básicamente estaba preguntando qué preguntó Sam Saffron justo antes de mí. – Tom

8

No, no es seguro para subprocesos. Me enfrenté a un problema similar hace unos días y decidí implementar el bloqueo gracias a un decorador. La ventaja es que hace que el código sea legible:

 
def threadsafe_function(fn): 
    """decorator making sure that the decorated function is thread safe""" 
    lock = threading.Lock() 
    def new(*args, **kwargs): 
     lock.acquire() 
     try: 
      r = fn(*args, **kwargs) 
     except Exception as e: 
      raise e 
     finally: 
      lock.release() 
     return r 
    return new 

class X: 
    var = 0 

    @threadsafe_function  
    def inc_var(self): 
     X.var += 1  
     return X.var 
+0

fuera del tema, pero ¿puede eliminar las dos llamadas a lock.release() a la sección "else:" después del manejador de excepciones? –

+0

¿Te refieres en una sección finalmente? Hacer eso en otra persona no se publicaría cuando se produzca una excepción – luc

+0

ah sí, eso es lo que quise decir. ¡Gracias! –

Cuestiones relacionadas