2009-07-02 22 views
14

Stavo leggendo this question (che non c'è bisogno di leggere perché io copio quello che c'è ... volevo solo per dare spettacolo la mia ispirazione) ...Sta modificando una variabile di classe in python threadsafe?

Quindi, se ho una classe che conta quante istanze sono state create:

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

La mia domanda è, se creo oggetti Foo in più thread, è instance_count sta per essere corretto? Le variabili di classe sono sicure da modificare da più thread?

risposta

21

Non è protetto da thread anche su CPython. Provate a vedere di persona:

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 ragione è che, mentre INPLACE_ADD è atomica sotto GIL, l'attributo è ancora caricato e memorizzare (vedi dis.dis (Foo .__ init__)). Utilizzare un blocco per serializzare l'accesso alla variabile di classe:

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

Credo nel tuo secondo esempio che vuoi che il target Thread sia interlocked_inc anziché inc_by. – tgray

+0

Grazie, corretto. Talvolta la programmazione di copia e incolla troppo liberale mi raggiunge. –

+0

Grazie Ats Anasma :-). Questo è come sospettavo. Grazie per avermelo dimostrato. Come sottolinea tgray, il tuo secondo obiettivo dovrebbe essere interlocked_inc. Ma una volta cambiato ... sembra impeccabile. – Tom

-4

Direi che è thread-safe, almeno per l'implementazione CPython. Il GIL farà in modo che tutti i tuoi "thread" vengano eseguiti in modo sequenziale, in modo che non possano interferire con il numero di riferimenti.

+2

È Foo.instance_count + = 1 e unità di lavoro atomica? –

+0

Forse non capisco come funziona il GIL ... ma continuo a non vederlo. Can not Thread1 ha letto istanza_count. Quindi il thread1 si interrompe. Thread2 legge instance_count, quindi si arresta. Thread1 modifica e scrive. Thread2 scrive. Quindi perdi un incremento? In che modo GIL garantisce che il thread attraversi l'intera operazione + =? – Tom

+0

Ha, fondamentalmente chiedevo cosa mi avesse chiesto Sam Saffron poco prima di me. – Tom

8

No, non è thread-safe. Ho affrontato un problema simile qualche giorno fa e ho scelto di implementare il lucchetto grazie a un decoratore. Il vantaggio è che rende leggibile il codice:

 
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

off-topic, ma è possibile rimuovere le due chiamate lock.release() in una sezione "else:" dopo il gestore di eccezioni? –

+0

Intendi in una sezione finale? Farlo in un altro non verrebbe rilasciato quando viene sollevata un'eccezione – luc

+0

ah sì, questo è quello che intendevo. Grazie! –

Problemi correlati