Quando useresti mai il blocco annidato?

Stavo leggendo nell’eccellente eBook di Albahari sul threading e mi sono imbattuto nel seguente scenario e ha detto che “un thread può bloccare ripetutamente lo stesso object in modo annidato (rientrante)”

lock (locker) lock (locker) lock (locker) { // Do something... } 

così come

 static readonly object _locker = new object(); static void Main() { lock (_locker) { AnotherMethod(); // We still have the lock - because locks are reentrant. } } static void AnotherMethod() { lock (_locker) { Console.WriteLine ("Another method"); } } 

Dalla spiegazione, qualsiasi thread bloccherà il primo blocco (più esterno) e verrà sbloccato solo dopo che il blocco esterno è uscito.

Dichiara che “il blocco annidato è utile quando un metodo chiama un altro all’interno di un blocco”

Perché è utile? Quando avresti bisogno di fare questo e quale problema risolve?

Non è tanto che sia utile farlo, poiché è utile che sia permesso. Considera come potresti spesso avere metodi pubblici che chiamano altri metodi pubblici. Se il metodo pubblico chiama in blocchi e il metodo pubblico che lo chiama deve bloccare l’ambito più ampio di ciò che fa, quindi essere in grado di usare i blocchi ricorsivi significa che puoi farlo.

Ci sono alcuni casi in cui potresti avere voglia di usare due oggetti di blocco, ma li userai insieme e quindi, se commetti un errore, c’è un grosso rischio di stallo. Se riesci a gestire l’ambito più ampio che viene dato al blocco, quindi utilizzando lo stesso object per entrambi i casi, e ricorsivamente nei casi in cui useresti entrambi gli oggetti, rimuoverai quei deadlock particolari.

Però…

Questa utilità è discutibile.

Nel primo caso citerò Joe Duffy :

La ricorsione indica in genere un’eccessiva semplificazione nella progettazione della sincronizzazione che spesso porta a un codice meno affidabile. Alcuni modelli utilizzano la ricorsione a lucchetto come un modo per evitare di dividere le funzioni in quelle che prendono i lucchetti e quelli che presumono che i lucchetti siano già stati presi. Ciò può certamente portare a una riduzione delle dimensioni del codice e quindi a un tempo di scrittura più breve, ma alla fine si traduce in un design più fragile. È sempre una buona idea inserire il codice in punti di ingresso pubblici che accettano blocchi non ricorsivi e le funzioni interne del lavoratore che asseriscono il blocco. Le chiamate di blocco ricorsive sono operazioni ridondanti che contribuiscono al sovraccarico delle prestazioni. Ma peggio, a seconda della ricorsione può rendere più difficile capire il comportamento di sincronizzazione del tuo programma, in particolare a quali limiti gli invarianti dovrebbero contenere. Di solito vorremmo dire che la prima riga dopo l’acquisizione di un blocco rappresenta un “punto sicuro” invariante per un object, ma non appena viene introdotta la ricorsione, questa affermazione non può più essere fatta con sicurezza. Ciò a sua volta rende più difficile assicurare un comportamento corretto e affidabile quando composto dynamicmente.

(Joe ha altro da dire sull’argomento altrove nel suo blog e nel suo libro sulla programmazione concorrente ).

Il secondo caso è bilanciato dai casi in cui l’entrata del blocco ricorsivo fa solo diversi tipi di deadlock, o aumenta la percentuale di contesa così alta che potrebbero esserci anche deadlock ( Questo tizio dice che preferirebbe solo per colpire un deadlock la prima volta che hai ricambiato, non sono d’accordo: preferirei che fosse solo una grande eccezione a far cadere la mia app con una bella traccia dello stack).

Una delle cose peggiori, si semplifica al momento sbagliato: quando si scrive codice, è più semplice usare la ricorsione della serratura piuttosto che dividere di più le cose e pensare più a fondo su cosa dovrebbe essere il blocco quando. Tuttavia, quando esegui il debug del codice, il fatto che lasciare un lucchetto non significa che lasciare quel lucchetto complica le cose. Che brutta cosa – è quando pensiamo di sapere che cosa stiamo facendo quel complicato codice è una tentazione di essere goduto nel tuo tempo libero, così non ti puoi sbizzarrire mentre sei sul cronometro, e quando ci siamo accorti di aver incasinato quel vogliamo soprattutto che le cose siano belle e semplici.

Davvero non vuoi mischiarli con le variabili di condizione.

Ehi, i thread POSIX li hanno solo per il coraggio!

Almeno la parola chiave lock significa che evitiamo la possibilità di non avere corrispondenza con Monitor.Exit() s per ogni Monitor.Enter() s che rende meno probabili alcuni dei rischi. Fino al momento in cui devi fare qualcosa al di fuori di quel modello.

Con classi di chiusura più recenti, .NET semplifica l’utilizzo della ricorsione da blocco, senza bloccare quelli che utilizzano modelli di codifica più vecchi. ReaderWriterLockSlim ha un sovraccarico del costruttore che consente di utilizzarlo come ricorsione, ma il valore predefinito è LockRecursionPolicy.NoRecursion .

Spesso nell’affrontare questioni di concorrenza dobbiamo prendere una decisione tra una tecnica più complessa che potrebbe potenzialmente darci una concorrenza migliore ma che richiede molta più attenzione per essere sicuri della correttezza rispetto a una tecnica più semplice che potrebbe potenzialmente dare una concorrenza peggiore, ma dove è più facile essere sicuri della correttezza. L’uso ricorsivo dei blocchi ci fornisce una tecnica in cui terremo bloccati i blocchi più a lungo e avremo una concorrenza meno buona, e inoltre saremo meno sicuri della correttezza e abbiamo un debug più difficile.

Diciamo che hai due metodi pubblici, A() e B() , che richiedono entrambi lo stesso blocco.

Inoltre, diciamo che A() chiama B()

Poiché il client può anche chiamare direttamente B() , è necessario bloccare entrambi i metodi.
Pertanto, quando viene chiamato A() , B() prenderà il blocco una seconda volta.

Se si dispone di una risorsa che si desidera controllare in modo esclusivo, ma molti metodi agiscono su questa risorsa. Un metodo potrebbe non essere in grado di assumere che sia bloccato, quindi lo bloccherà all’interno del suo metodo. Se è bloccato nel metodo esterno AND metodo interno, fornisce una situazione simile all’esempio nel libro. Non riesco a vedere un momento in cui vorrei bloccare due volte nello stesso blocco di codice.