2013-04-11 15 views
5

Sono nuovo in Python e sto attualmente cercando di imparare il threading. Sono stanco di usare i lock per rendere le mie risorse thread-safe perché non sono intrinsecamente legate alla risorsa, quindi sono costretto a dimenticarmi di acquisirle e/o rilasciarle ogni volta che il mio codice interagisce con la risorsa. Invece, mi piacerebbe essere in grado di "avvolgere" (o decorare?) Un oggetto in modo che tutti i suoi metodi e getter/setter di attributi siano atomici. qualcosa di simile:Come decorare un oggetto Python con un mutex

È possibile? In tal caso, qual è il modo di "miglior prassi" per implementarlo?

MODIFICA: una buona risposta alla domanda precedente è disponibile in How to make built-in containers (sets, dicts, lists) thread safe?. Però; come hanno dimostrato sia abarnert che jsbueno, la soluzione che ho proposto (automatizzare i lock) non è generalmente una buona idea, perché determinare la granularità corretta delle operazioni atomiche richiede un po 'di intelligenza ed è probabilmente difficile (o impossibile) automatizzare correttamente.

Il problema rimane comunque che i blocchi non sono vincolati in alcun modo alle risorse che intendono proteggere, quindi la mia nuova domanda è: Qual è un buon modo per associare un lucchetto a un oggetto?

Soluzione proposta # 2: Immagino che ci potrebbe essere un modo per associare un blocco per un oggetto in modo tale che il tentativo di accedere a tale oggetto senza prima acquisire il blocco genera un errore, ma posso vedere come sia potuto ottenere ingannevole .

MODIFICA: il seguente codice non è molto pertinente alla domanda. L'ho pubblicato per dimostrare che avevo provato a risolvere il problema me stesso e mi sono perso prima di pubblicare questa domanda.

Per la cronaca, ho scritto il seguente codice, ma non funziona:

import threading  
import types 
import inspect 

class atomicObject(object): 

    def __init__(self, obj): 
     self.lock = threading.RLock() 
     self.obj = obj 

     # keep track of function handles for lambda functions that will be created 
     self.funcs = [] 

     # loop through all the attributes of the passed in object 
     # and create wrapped versions of each attribute 
     for name in dir(self.obj): 
      value = getattr(self.obj, name) 
      if inspect.ismethod(value): 
       # this is where things get really ugly as i try to work around the 
       # limitations of lambda functions and use eval()... I'm not proud of this code 
       eval("self.funcs.append(lambda self, *args, **kwargs: self.obj." + name + "(*args, **kwargs))") 
       fidx = str(len(self.funcs) - 1) 
       eval("self." + name + " = types.MethodType(lambda self, *args, **kwargs: self.atomize(" + fidx + ", *args, **kwargs), self)") 

    def atomize(self, fidx, *args, **kwargs): 
     with self.lock: 
      return self.functions[fidx](*args, **kwargs) 

posso creare un atomicObject (dict()), ma quando provo ad aggiungere un valore all'oggetto , Ottengo l'errore; "atomicObject non supporta l'assegnazione degli articoli".

+0

Inoltre, questo codice non si avvicina nemmeno alla corsa. Ti mancano due punti dopo l'istruzione 'with', hai il nome sbagliato per il tipo' Lock', e non ho idea di cos'altro potrebbe andare oltre. Come ti aspetti che eseguiamo il debugging per te se non siamo nemmeno in grado di iniziare? – abarnert

+0

Inoltre, di solito non è un'idea così buona come sembra. Per esempio, se creo 'd = atomicObject (dict())', quindi 'd ['abc'] = 3', quindi' d ['abc'] + = 1', che non è atomico - atomicamente legge 'd ['abc']', quindi rilascia il blocco, quindi scrive atomicamente 'd ['abc']', sovrascrivendo qualsiasi altra scrittura fatta nell'intervallo di tempo. (Immagina che 'd' fosse un segnalino e che avessi 20 thread tutti cercando di fare +1 allo stesso tempo. Invece di salire +20, probabilmente salirà +1 o +2 circa.) – abarnert

+0

I Mi dispiace per il codice sciatto. È il prodotto della tortura e della frustrazione. Penso di aver corretto alcuni degli errori, ma in genere ho incluso il mio codice come cortesia. Speravo che qualcuno potesse indicarmi la giusta direzione dato che sono ovviamente perso. La tua risposta è stata di enorme aiuto. Grazie! – arachnivore

risposta

4

È molto difficile distinguere dal tuo esempio non in esecuzione e dal tuo pasticcio del codice eval, ma c'è almeno un errore evidente.

Prova questo nel vostro interprete interattivo:

>>> d = dict() 
>>> inspect.ismethod(d.__setitem__) 

Come the docs dicono, ismethod:

Restituisce vero se l'oggetto è un metodo vincolato scritto in Python.

Un metodo-wrapper scritto in C (o .NET, Java, il successivo spazio di lavoro in basso, ecc. Per altre implementazioni Python) non è un metodo associato scritto in Python.

Probabilmente volevi solo callable o inspect.isroutine qui.

non posso dire se questo è l'unico problema, perché se posso correggere gli errori di sintassi e gli errori di nome e questo bug, la seconda linea eval genera codice illegale come questo:

self.__cmp__ = types.MethodType(lambda self, *args, **kwargs: self.atomize(0, *args, **kwargs) self) 

... e io non sono sicuro di cosa stavi cercando di fare lì.


Davvero non dovresti provare a creare e eval nulla. Per assegnare attributi dinamicamente per nome, utilizzare setattr. E non hai bisogno di complicati lambda s. Basta definire la funzione avvolta con un normale def; il risultato è un valore locale perfettamente valido che puoi trasmettere, esattamente come un lambda, tranne che ha un nome.

Per di più, cercare di avvolgere i metodi in modo statico al momento della creazione è difficile e presenta alcuni aspetti negativi importanti. Ad esempio, se la classe di cui esegui il wrapping ha metodi generati dinamicamente, non li avvolgi. Per la maggior parte del tempo, è meglio farlo in modo dinamico, al momento della chiamata, con __getattr__. (Se sei preoccupato per il costo della creazione delle funzioni wrapper ogni volta che vengono chiamate ... In primo luogo, non preoccuparti se non profili effettivamente e scopri che è un collo di bottiglia, perché probabilmente non lo sarà. Ma, se è, si può facilmente aggiungere una cache di funzioni generate)

Quindi, ecco una molto più semplice, e di lavoro, l'attuazione di quello che penso che stai cercando di fare.

class atomicObject(object): 

    def __init__(self, obj): 
     self.lock = threading.Lock() 
     self.obj = obj 

    def __getattr__(self, name): 
     attr = getattr(self.obj, name) 
     print(attr) 
     if callable(attr): 
      def atomized(*args, **kwargs): 
       with self.lock: 
        attr(*args, **kwargs) 
      return atomized 
     return attr 

Tuttavia, questo isn farai effettivamente quello che vuoi Ad esempio:

>>> d = atomicObject(dict()) 
>>> d.update({'a': 4}) # works 
>>> d['b'] = 5 
TypeError: 'atomicObject' object does not support item assignment 

Perché ciò accade? Hai una __setitem__, e funziona:

>>> d.__setitem__ 
<method-wrapper '__setitem__' of dict object at 0x100706830> 
>>> d.__setitem__('b', 5) # works 

Il problema è che, come the docs implica, metodi speciali vengono ricercati sulla classe, non l'oggetto. E la classe atomicObject non ha un metodo __setitem__.

In realtà, questo significa che non si può nemmeno utilmente stampare l'oggetto, perché basta avere il default __str__ e __repr__ da object:

>>> d 
<__main__.atomicObject object at 0x100714690> 
>>> print(d) 
<__main__.atomicObject object at 0x100714690> 
>>> d.obj #cheating 
{'a': 4, 'b': 5} 

Quindi, la cosa giusta da fare qui è quello di scrivere una funzione che definisce una classe wrapper per ogni classe, quindi eseguire:

>>> AtomicDict = make_atomic_wrapper(dict) 
>>> d = AtomicDict() 

Ma, anche dopo aver fatto tutto questo ... raramente è un'idea così valida come sembra.

Considerate questo:

d = AtomicDict() 
d['abc'] = 0 
d['abc'] += 1 

L'ultima riga non è atomica. C'è un atomico __getitem__, quindi un atomico separato __setitem__.

Potrebbe non sembrare un grosso problema, ma immagina che d venga utilizzato come contatore. Hai 20 thread tutti cercando di fare d['abc'] += 1 allo stesso tempo. Il primo a entrare nello __getitem__ tornerà a 0. E se è l'ultimo a entrare sul __setitem__, lo imposterà su 1.

Provare a eseguire this example.Con il blocco corretto, dovrebbe sempre stampare 2000. Ma sul mio portatile, di solito è più vicino a 125.

+0

Grazie. Questo è stato estremamente utile. Quel codice confuso prodotto dalla dichiarazione di valutazione è stato preso in prestito da [qui] (http://stackoverflow.com/questions/357997/does-python-have-something-like-anonymous-inner-classes-of-java) (seconda risposta, quinto commento). Fai un buon punto sull'idea atomicObject() che è approssimativa. Dovrò riconsiderare questa idea. – arachnivore

+0

@abarnet - Ho usato il tuo codice di esempio qui sotto - solo per farti sapere – jsbueno

2

Ho riflettuto sulla tua domanda, e sarebbe stato un po 'complicato - devi proxy non solo tutti i metodi oggetto con la classe Atomic, che possono essere eseguiti correttamente scrivendo un metodo __getattribute__ - ma, per gli operatori stessi, è necessario fornire l'oggetto proxy con una classe che fornisce lo stesso "carattere magico di sottolineatura" metodi come classe degli oggetti originali - cioè, devi creare dinamicamente una classe proxy - altrimenti l'utilizzo da parte dell'operatore non sarà atomico.

È fattibile - ma dato che sei nuovo in Python, è possibile eseguire import this sul prompt interattivo, e fra i diversi orientamenti/consigli che appaiono si vedrà: "" "Se l'implementazione è difficile per spiegare, è una cattiva idea """ :-)

Il che ci porta a:. Utilizzando discussioni in Python è generalmente una cattiva idea. Tranne per il codice quasi banale con un sacco di I/O di blocco, preferirai un altro approccio, poiché il threading in Python non consente al normale codice Python di utilizzare più core CPU, ad esempio - c'è solo un singolo thread di codice Python in esecuzione contemporaneamente: cerca "Python GIL" per capire perché - (eccezione, se gran parte del tuo codice viene spesa in codice nativo intensivo di calcolo, come le funzioni di Numpy).

Ma si preferisce scrivere si r programma di usare chiamate asincrone utilizzando uno dei vari quadri disponibili per questo, o per prendere facilmente vantaggio da più di un core, utilizzare multiprocessing invece di threading - che crea in pratica un processo per "thread" - e richiede che tutta quella condivisione sia fatta esplicitamente.

+0

Il motivo per cui utilizzo il threading non è il rendimento. Capisco che è solo psudo-threading. Sto solo cercando di rendere le cose asincrone. Sto lavorando su un dungeon multiutente basato su testo, quindi le prestazioni non rappresentano una preoccupazione enorme. Finora, il threading sembra essere piuttosto diretto (specialmente con l'uso di Queue), quindi sono curioso del perché la consideri una cattiva idea? – arachnivore

+1

@arachnivore: Molte persone sembrano credere che "il threading sia cattivo" (o "in Python" o "full stop"), ma è perché sono troppo generose. Per il parallelismo della CPU, i thread Python sono inutili e devi usare i processi. Per i server massivamente concomitanti (c10k), i thread sono troppo pesanti, e devi usare loop di eventi espliciti e callback/greenlet/coroutine di generatore. Ma per i client di rete, i server che sono pensati solo per gestire una dozzina di utenti, applicazioni GUI, ecc., I thread sono spesso la soluzione migliore. – abarnert

+0

@arachnivore: Inoltre: trattare dati (mutabili) condivisi è un problema difficile, e la gente lo usa spesso come argomento contro i thread, ma si applica altrettanto alle altre forme di concorrenza. (C'è un motivo per cui tutto da 'twisted' a' multiprocessing' consiglia di evitarlo.) – abarnert

0

Nonostante la mia altra risposta - che ha considerazioni valide su Python threading, e per sua volta un esistente oggetto in un oggetto "atomico" bloccato - se si sta definendo la classe dell'oggetto da bloccare atomicamente, il tutto è un ordine di grandezza più semplice.

Si può fare un decoratore di funzioni per far funzionare le funzioni con un lucchetto con quattro linee. Con ciò è possibile costruire un decoratore di classe che blocchi atomicamente tutti i metodi e le proprietà di una data classe.

Il soffietto codice funziona con Python 2 e 3 (avevo usato @ di abarnet esempio per la funzione di chiamate - e si basava sulla mia "di debug stampa" per l'esempio di classe.)

import threading 
from functools import wraps 

#see http://stackoverflow.com/questions/15960881/how-to-decorate-a-python-object-with-a-mutex/15961762#15960881 

printing = False 

lock = threading.Lock() 
def atomize(func): 
    @wraps(func) 
    def wrapper(*args, **kw): 
     with lock: 
      if printing: 
       print ("atomic") 
      return func(*args, **kw) 
    return wrapper 

def Atomic(cls): 
    new_dict = {} 
    for key, value in cls.__dict__.items(): 
     if hasattr(value, "__get__"): 
      def get_atomic_descriptor(desc): 
       class Descriptor(object): 
        @atomize 
        def __get__(self, instance, owner): 
         return desc.__get__(instance, owner) 
        if hasattr(desc, "__set__"): 
         @atomize 
         def __set__(self, instance, value): 
          return desc.__set__(instance, value) 
        if hasattr(desc, "__delete__"): 
         @atomize 
         def __delete__(self, instance): 
          return desc.__delete__(instance) 
       return Descriptor() 
      new_dict[key] = get_atomic_descriptor(value) 
     elif callable(value): 
      new_dict[key] = atomize(value) 
     else: 
      new_dict[key] = value 
    return type.__new__(cls.__class__, cls.__name__, cls.__bases__, new_dict) 


if __name__ == "__main__": # demo: 
    printing = True 

    @atomize 
    def sum(a,b): 
     return a + b 

    print (sum(2,3)) 

    @Atomic 
    class MyObject(object): 
     def _get_a(self): 
      return self.__a 

     def _set_a(self, value): 
      self.__a = value + 1 

     a = property(_get_a, _set_a) 

     def smurf(self, b): 
      return self.a + b 

    x = MyObject() 
    x.a = 5 
    print(x.a) 
    print (x.smurf(10)) 

    # example of atomized function call - based on 
    # @abarnet's code at http://pastebin.com/MrtR6Ufh 
    import time, random 
    printing = False 
    x = 0 

    def incr(): 
     global x 
     for i in range(100): 
      xx = x 
      xx += 1 
      time.sleep(random.uniform(0, 0.02)) 
      x = xx 

    def do_it(): 
     threads = [threading.Thread(target=incr) for _ in range(20)] 
     for t in threads: 
      t.start() 
     for t in threads: 
      t.join() 

    do_it() 
    print("Unlocked Run: ", x) 

    x = 0 
    incr = atomize(incr) 
    do_it() 
    print("Locked Run: ", x) 

NB: anche se "eval" e "exec" sono disponibili in Python, il codice serio raramente - e intendo lo raramente - serve entrambi. Persino i decoratori complessi che ricreano le funzioni possono eseguire l'introspezione piuttosto che affidarsi alla compilazione di stringhe tramite eval.

+0

Buon lavoro che lega insieme tutti i pezzi separati. Un'ultima cosa da sottolineare: l'atomizzazione dell'intera funzione 'incr' risolve i blocchi troppo fini che consentono le razze, ma solo usando serrature troppo grosse che serializzano tutti i thread. (Ogni thread tiene il lock per l'intero corso della sua corsa, quindi possono essere eseguiti solo uno dopo l'altro.) Quello che devi veramente bloccare qui è all'interno del ciclo 'for' (anche quello significa la maggior parte del tempo, 19 thread attenderanno 1 thread per dormire per 10ms, ma non c'è opzione migliore), e non c'è nessun posto ovvio per aggiungere quel lock dall'esterno. – abarnert

+1

Più in generale: trovare la granularità della serratura giusta è la sfida più difficile nella programmazione con thread e non esiste un punto magico.Tranne, naturalmente, per sbarazzarsi della necessità di blocchi a livello di applicazione, condividere solo dati immutabili (trasformare dati mutabili in trasformazioni immutabili), passare a attori che trasmettono messaggi, utilizzare transazioni, ecc. – abarnert

+0

@abarnet: rispetto al primo commento: bloccare l'intero metodo e attribuire gli accessi è ciò che l'OP chiedeva. Sono d'accordo che è male, e l'esecuzione del frammento di cui sopra è "vale 1000 parole" :-) – jsbueno

0

Ritorno a questi anni più tardi. Penso che un context manager sia la soluzione ideale al mio problema originale. So che i blocchi supportano la gestione del contesto, ma il problema di applicare la relazione tra il blocco e la risorsa bloccata rimane ancora. Invece immagino che qualcosa del genere sia buona:

class Locked: 
    def __init__(self, obj): 
     super().__init__() 
     self.__obj = obj 
     self.lock = threading.RLock() 

    def __enter__(self): 
     self.lock.acquire() 
     return self.__obj 

    def __exit__(self, *args, **kwargs): 
     self.lock.release() 


guard = Locked(dict()) 

with guard as resource: 
    do_things_with(resource) 
Problemi correlati