Blocco ricorsivo (Mutex) vs Blocco non ricorsivo (Mutex)


183

POSIX consente ai mutex di essere ricorsivi. Ciò significa che lo stesso thread può bloccare lo stesso mutex due volte e non si bloccherà. Ovviamente deve anche sbloccarlo due volte, altrimenti nessun altro thread può ottenere il mutex. Non tutti i sistemi che supportano pthread supportano anche i mutex ricorsivi, ma se vogliono essere conformi a POSIX, devono farlo .

Altre API (API di livello più elevato) di solito offrono anche mutex, spesso chiamati Lock. Alcuni sistemi / linguaggi (ad esempio Cocoa Objective-C) offrono mutex sia ricorsivi che non ricorsivi. Alcune lingue offrono anche solo l'una o l'altra. Ad esempio, in Java i mutex sono sempre ricorsivi (lo stesso thread può "sincronizzare" due volte sullo stesso oggetto). A seconda delle altre funzionalità di thread che offrono, non avere mutex ricorsivi potrebbe non essere un problema, in quanto possono essere facilmente scritti da soli (ho già implementato me stesso mutex ricorsivi sulla base di operazioni mutex / condition più semplici).

Cosa non capisco davvero: a cosa servono i mutex non ricorsivi? Perché dovrei avere un deadlock di thread se blocca lo stesso mutex due volte? Anche i linguaggi di alto livello che potrebbero evitarlo (ad esempio test se questo si bloccherà e genererà un'eccezione se lo fa) di solito non lo fanno. Lasceranno invece il deadlock del thread.

Questo è solo per i casi, in cui lo blocco accidentalmente due volte e lo sblocco solo una volta e in caso di un mutex ricorsivo, sarebbe più difficile trovare il problema, quindi invece ho immediatamente un deadlock per vedere dove appare il blocco errato? Ma non posso fare lo stesso con un contatore dei blocchi restituito durante lo sblocco e in una situazione in cui sono sicuro di aver rilasciato l'ultimo blocco e il contatore non è zero, posso lanciare un'eccezione o registrare il problema? O c'è qualche altro caso d'uso più utile di mutex non ricorsivi che non riesco a vedere? O è forse solo una prestazione, poiché un mutex non ricorsivo può essere leggermente più veloce di uno ricorsivo? Tuttavia, l'ho provato e la differenza non è poi così grande.

Risposte:


154

La differenza tra un mutex ricorsivo e non ricorsivo ha a che fare con la proprietà. Nel caso di un mutex ricorsivo, il kernel deve tenere traccia del thread che ha effettivamente ottenuto il mutex la prima volta in modo da poter rilevare la differenza tra ricorsione rispetto a un thread diverso che dovrebbe invece bloccare. Come ha sottolineato un'altra risposta, c'è una domanda sul sovraccarico aggiuntivo di questo sia in termini di memoria per memorizzare questo contesto, sia i cicli necessari per mantenerlo.

Tuttavia , ci sono anche altre considerazioni in gioco qui.

Poiché il mutex ricorsivo ha un senso di proprietà, il thread che acquisisce il mutex deve essere lo stesso thread che rilascia il mutex. Nel caso di mutex non ricorsivi, non vi è alcun senso di proprietà e qualsiasi thread di solito può rilasciare il mutex indipendentemente da quale thread ha originariamente preso il mutex. In molti casi, questo tipo di "mutex" è in realtà più un'azione di semaforo, in cui non si utilizza necessariamente il mutex come dispositivo di esclusione ma lo si utilizza come dispositivo di sincronizzazione o di segnalazione tra due o più thread.

Un'altra proprietà che deriva da un senso di proprietà in un mutex è la capacità di supportare l'ereditarietà prioritaria. Poiché il kernel può tracciare il thread che possiede il mutex e anche l'identità di tutti i bloccanti, in un sistema con thread prioritario diventa possibile innalzare la priorità del thread che attualmente possiede il mutex alla priorità del thread con priorità più alta che attualmente sta bloccando sul mutex. Questa eredità impedisce il problema dell'inversione di priorità che può verificarsi in tali casi. (Notare che non tutti i sistemi supportano l'ereditarietà prioritaria su tali mutex, ma è un'altra caratteristica che diventa possibile tramite la nozione di proprietà).

Se fai riferimento al classico kernel VxWorks RTOS, definiscono tre meccanismi:

  • mutex : supporta la ricorsione e, facoltativamente, l'ereditarietà prioritaria. Questo meccanismo è comunemente usato per proteggere sezioni critiche di dati in modo coerente.
  • semaforo binario : nessuna ricorsione, nessuna eredità, semplice esclusione, acquirente e donatore non deve essere lo stesso thread, disponibile la trasmissione. Questo meccanismo può essere utilizzato per proteggere sezioni critiche, ma è anche particolarmente utile per la segnalazione coerente o la sincronizzazione tra thread.
  • conteggio dei semafori : nessuna ricorsione o ereditarietà, agisce come un contatore di risorse coerente da qualsiasi conteggio iniziale desiderato, i thread si bloccano solo dove il conteggio netto rispetto alla risorsa è zero.

Ancora una volta, questo varia in qualche modo in base alla piattaforma, in particolare a ciò che chiamano queste cose, ma ciò dovrebbe essere rappresentativo dei concetti e dei vari meccanismi in gioco.


9
la tua spiegazione sul mutex non ricorsivo suonava più come un semaforo. Un mutex (sia ricorsivo che non ricorsivo) ha una nozione di proprietà.
Jay D,

@JayD È molto confuso quando le persone discutono di cose come queste ... quindi chi è l'entità che definisce queste cose?
Pacerier,

13
@Pacerier Lo standard pertinente. Questa risposta, ad esempio, è errata per posix (pthreads), dove sbloccare un normale mutex in un thread diverso dal thread che lo ha bloccato è un comportamento indefinito, mentre fare lo stesso con un controllo degli errori o un mutex ricorsivo si traduce in un codice di errore prevedibile. Altri sistemi e standard potrebbero comportarsi in modo molto diverso.
nn.

Forse questo è ingenuo, ma avevo l'impressione che l'idea centrale di un mutex sia che il thread di blocco sblocca il mutex e quindi altri thread potrebbero fare lo stesso. Da computing.llnl.gov/tutorials/pthreads :
user657862

2
@curiousguy - una versione broadcast libera tutti i thread bloccati sul semaforo senza specificarlo esplicitamente (rimane vuoto) mentre un normale comando binario rilascerebbe il thread solo in testa alla coda di attesa (supponendo che ne sia bloccato uno).
Tall Jeff

123

La risposta non è efficienza. I mutex non rientranti portano a un codice migliore.

Esempio: A :: foo () acquisisce il blocco. Quindi chiama B :: bar (). Questo ha funzionato bene quando l'hai scritto. Ma qualche tempo dopo qualcuno cambia B :: bar () per chiamare A :: baz (), che acquisisce anche il blocco.

Bene, se non hai mutex ricorsivi, questo deadlock. Se li hai, funziona, ma potrebbe rompersi. A :: foo () potrebbe aver lasciato l'oggetto in uno stato incoerente prima di chiamare bar (), supponendo che baz () non potesse essere eseguito perché acquisisce anche il mutex. Ma probabilmente non dovrebbe funzionare! La persona che ha scritto A :: foo () ha ipotizzato che nessuno potesse chiamare A :: baz () allo stesso tempo - questa è l'intera ragione per cui entrambi quei metodi hanno acquisito il blocco.

Il modello mentale giusto per usare i mutex: il mutex protegge un invariante. Quando si tiene il mutex, l'invariante può cambiare, ma prima di rilasciare il mutex, viene ripristinato l'invariante. I blocchi rientranti sono pericolosi perché la seconda volta che acquisisci il blocco non puoi più essere sicuro che l'invariante sia più vero.

Se sei soddisfatto dei blocchi rientranti, è solo perché non hai dovuto eseguire il debug di un problema come questo prima. Java ha blocchi non rientranti in questi giorni in java.util.concurrent.locks, tra l'altro.


4
Mi ci è voluto un po 'per capire cosa stavi dicendo che l'invariante non era valido quando hai afferrato il lucchetto una seconda volta. Buon punto! E se fosse un blocco di lettura / scrittura (come il ReadWriteLock di Java) e tu acquisissi il blocco di lettura e poi riacquistassi il blocco di lettura una seconda volta nello stesso thread. Non invalideresti un invariante dopo aver acquisito un blocco di lettura giusto? Quindi, quando acquisisci il secondo blocco di lettura, l'invariante è ancora vero.
Dgrant

1
@Jonathan Java ha blocchi non rientranti in questi giorni in java.util.concurrent.locks ??
user454322

1
+1 Suppongo che l'uso più comune per il blocco rientrante sia all'interno di una singola classe, in cui alcuni metodi possono essere chiamati sia da pezzi di codice protetti sia da quelli non protetti. Questo può in realtà essere sempre preso in considerazione. @ user454322 Certo, Semaphore.
maaartinus,

1
Perdonate il mio malinteso, ma non vedo quanto questo sia rilevante per il mutex. Supponiamo che non ci siano multithreading e lock coinvolti, A::foo()potrebbe aver lasciato l'oggetto in uno stato incoerente prima di chiamare A::bar(). Cosa c'entra il mutex, ricorsivo o no, con questo caso?
Siyuan Ren,

1
@SiyuanRen: il problema è riuscire a ragionare localmente sul codice. Le persone (almeno io) sono addestrate a riconoscere le regioni bloccate come invarianti, ovvero nel momento in cui acquisisci il blocco nessun altro thread sta modificando lo stato, quindi gli invarianti nella regione critica sono in possesso. Questa non è una regola difficile e puoi programmare con gli invarianti che non vengono tenuti in considerazione, ma ciò renderebbe il tuo codice più difficile da ragionare e mantenere. Lo stesso accade nella modalità a thread singolo senza mutex, ma non siamo addestrati a ragionare localmente intorno alla regione protetta.
David Rodríguez - dribeas,

92

Come scritto dallo stesso Dave Butenhof :

"Il più grande di tutti i grandi problemi con i mutex ricorsivi è che ti incoraggiano a perdere completamente traccia del tuo schema di blocco e portata. Questo è mortale. Il male. È il" mangiatore di fili ". Tieni i blocchi per il tempo assolutamente più breve possibile. Periodo. Sempre. Se stai chiamando qualcosa con un lucchetto tenuto semplicemente perché non sai che è bloccato, o perché non sai se la chiamata ha bisogno del mutex, allora lo stai trattenendo troppo a lungo. mirare a un fucile a pompa e premere il grilletto. Presumibilmente hai iniziato a utilizzare i thread per ottenere la concorrenza, ma hai appena PREVENUTO la concorrenza. "


9
Nota anche la parte finale della risposta di Butenhof: ...you're not DONE until they're [recursive mutex] all gone.. Or sit back and let someone else do the design.
user454322

2
Dice anche che usare un singolo mutex ricorsivo globale (la sua opinione è che ne hai bisogno solo uno) va bene come stampella per rimandare consapevolmente il duro lavoro di comprensione delle invarianze di una libreria esterna quando inizi a usarlo in codice multithread. Ma non dovresti usare le stampelle per sempre, ma alla fine investi il ​​tempo per capire e correggere gli invarianti di concorrenza del codice. Quindi potremmo parafrasare che l'uso del mutex ricorsivo è un debito tecnico.
FooF,

13

Il modello mentale giusto per usare i mutex: il mutex protegge un invariante.

Perché sei sicuro che questo sia davvero il modello mentale giusto per usare i mutex? Penso che il modello giusto stia proteggendo i dati ma non gli invarianti.

Il problema della protezione degli invarianti si presenta anche nelle applicazioni a thread singolo e non ha nulla in comune con il multi-threading e i mutex.

Inoltre, se hai bisogno di proteggere gli invarianti, puoi comunque usare un semaforo binario che non è mai ricorsivo.


Vero. Esistono meccanismi migliori per proteggere un invariante.
ActiveTrayPrntrTagDataStrDrvr

8
Questo dovrebbe essere un commento alla risposta che ha offerto quella dichiarazione. I mutex non solo proteggono i dati, ma proteggono anche gli invarianti. Prova a scrivere un semplice contenitore (il più semplice è uno stack) in termini di atomica (in cui i dati si proteggono) invece di mutex e capirai l'affermazione.
David Rodríguez - dribeas,

I mutex non proteggono i dati, proteggono un invariante. Tale invariante può tuttavia essere utilizzato per proteggere i dati.
Jon Hanna,

4

Uno dei motivi principali per cui i mutex ricorsivi sono utili è nel caso di accedere ai metodi più volte dallo stesso thread. Ad esempio, dire se il blocco mutex sta proteggendo una banca A / c per il prelievo, quindi se è presente anche una commissione associata a quel prelievo, è necessario utilizzare lo stesso mutex.


4

L'unico caso utile per il mutex ricorsivo è quando un oggetto contiene più metodi. Quando uno dei metodi modifica il contenuto dell'oggetto, quindi deve bloccare l'oggetto prima che lo stato sia nuovamente coerente.

Se i metodi usano altri metodi (es: addNewArray () chiama addNewPoint () e si finalizza con recheckBounds ()), ma una di queste funzioni da sola deve bloccare il mutex, allora il mutex ricorsivo è vantaggioso per tutti.

Per ogni altro caso (risolvere solo codici errati, usarlo anche in oggetti diversi) è chiaramente sbagliato!


1

A cosa servono i mutex non ricorsivi?

Sono assolutamente utili quando devi assicurarti che il mutex sia sbloccato prima di fare qualcosa. Questo perché pthread_mutex_unlockpuò garantire che il mutex sia sbloccato solo se non ricorsivo.

pthread_mutex_t      g_mutex;

void foo()
{
    pthread_mutex_lock(&g_mutex);
    // Do something.
    pthread_mutex_unlock(&g_mutex);

    bar();
}

Se g_mutexnon è ricorsivo, è garantito che il codice sopra riportato chiami bar()con il mutex sbloccato .

Eliminando così la possibilità di un deadlock nel caso bar() verifichi una funzione esterna sconosciuta che potrebbe fare qualcosa che potrebbe comportare un altro thread che prova ad acquisire lo stesso mutex. Tali scenari non sono rari nelle applicazioni basate su pool di thread e nelle applicazioni distribuite, in cui una chiamata tra processi può generare un nuovo thread senza che il programmatore del client se ne renda conto. In tutti questi scenari è meglio invocare le suddette funzioni esterne solo dopo aver rilasciato il blocco.

Se g_mutexfosse ricorsivo, semplicemente non ci sarebbe modo di assicurarsi che sia sbloccato prima di effettuare una chiamata.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.