L'obiettivo è quello di far emergere dal disegno in modo evidente la politica di gestione dello stallo che si vuole seguire e aiutare ad applicarlo in modo consistente in tutta la applicazione. Elenco qui un repertorio di pattern che trovo utili quando si implementa la strategia di prevenzione del deadlock [1].
Ordered Lock
Quando l'esecuzione di un metodo e la sequenza che segue di chiamate ad altri metodi richiede di acquisire più lock insieme, si previene il deadlock acquisendo sempre i lock con ordine dato (per esempio i lock su istanze della classe ServerListener vanno fatti sempre prima di quelli sulla classe ConnectedClient che a loro volta vanno prima di quelli sulle istanze della classe Client).
Il pattern è descritto anche nel Portland Pattern Repository http://c2.com/cgi/wiki?OrderedLocks
Wait All Lock
L'altro modo di prevenire il deadlock è di acquisire tutti i lock che servono insieme per esempio usando il metodo Mutex.WaitAll()
Entrambi questi pattern suggeriscono di centralizzare l'acquisizione dei lock e va nella direzione opposta di incapsulare in ogni classe la gestione dei propri lock (per esempio la classe ServerListener si preoccupa in ogni metodo di acquisire i lock necessari per leggere e scrivere i suoi field e cosi fa la classe ConnectedClient etc.) Come fare ? Ho raccolto 2 idee.
Mediator
Questo approccio consiste nel separare la sequanza di chiamate tra metodi che provocano la acquisizione di più lock (per esempio la chiamata del metodo ServerListener.AcceptCOnnectionRequest() che internamente chiama ConnectedClient.AddNewClient() che a sua volta chiama new Client() ).
La sequenza di chiamate dirette viene spezzata e sostituita da una classe "mediatore" che fa le singole chiamate singolarmente una dopo l'altra e risponde agli eventi (un po come un form con i controlli) :
http://wiki.ugidotnet.org/default.aspx/UGIdotNETWiki/PatternMediator
Questa classe "mediatore" ora fa emergere quella che prima era una sequenza di chiamate nidificate come matriosche e ora è invece una lista piatta di operazioni. E cosi può assumersi la responsabilità di acquisire i lock (con uno qualunque dei 2 pattern sopra) senza ropmere l'incapsulamento delle classi.
Diventando cosi visibili tutti gli ordered lock diventa evidente se e quando può convenire sostituirle un ordered lock con un Coarse Grained Lock : http://martinfowler.com/eaaCatalog/coarseGrainedLock.html
Data Mapper
L'idea è di separare nel disegno le strutture dati condivise dalle classi che implementano la logica come fa il pattern Data Mapper (anche se il pattern è originariamente destinato a una tabella nel db mentre in questo caso interessa di una struttura dati condivisa) : http://martinfowler.com/eaaCatalog/dataMapper.html
Ogni sequenza di chiamate viene eseguita da una procedura che chiama i mapper per leggere le strutture dati condivise e valorizzare le istanze che implementano la logica, esegue le operazioni necessarie e richiama i mapper per aggiornare le strutture dati condivise. Ogni sequenza può essere implementata in un Transaction Script : http://martinfowler.com/eaaCatalog/transactionScript.html
Per far si che tutti i Transaction Script seguano in modo uniforme il modello di lock e di gestione dello stallo si puo far derivare tutti i transaction script dallo stesso Layer Supertype http://martinfowler.com/eaaCatalog/layerSupertype.html .
_____________________________________
[1] Richard C. Holt: Some Deadlock Properties of Computer Systems. ACM Comput. Surv. Vol.4 Num.3, Sett 1972
Tags : Progettazione Software |