Domanda

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 i pthread supportano anche i mutex ricorsivi, ma se vogliono essere POSIX conforme, devono .

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. Per esempio. in Java i mutex sono sempre ricorsivi (lo stesso thread può due volte "sincronizzare" sullo stesso oggetto). A seconda delle altre funzionalità di thread che offrono, non avere mutex ricorsivi potrebbe non essere un problema, poiché possono essere facilmente scritti da soli (ho già implementato me stesso mutex ricorsivi sulla base di più semplici operazioni mutex / condition).

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 testare se questo si bloccherà e generare 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 si trova il blocco errato appare? Ma non potrei 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.

È stato utile?

Soluzione

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" è davvero più un'azione 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 si fa riferimento al classico kernel VxWorks RTOS, questi definiscono tre meccanismi:

  • mutex : supporta la ricorsione e, facoltativamente, l'eredità 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à, funge da 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 a seconda della piattaforma, in particolare di ciò che chiamano queste cose, ma ciò dovrebbe essere rappresentativo dei concetti e dei vari meccanismi in gioco.

Altri suggerimenti

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 questi 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, l'invariante viene ristabilito. 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. A proposito, Java ha blocchi non rientranti in questi giorni in java.util.concurrent.locks, tra l'altro.

Come scritto dallo stesso Dave Butenhof :

" Il più grande di tutti i grandi problemi con i mutex ricorsivi è quello ti incoraggiano a perdere completamente traccia del tuo schema di blocco e scopo. Questo è mortale. Il male. È il "mangiatore di thread". Tieni le serrature per il tempo assolutamente più breve possibile. Periodo. Sempre. Se stai chiamando qualcosa con un lucchetto tenuto semplicemente perché non sai che è tenuto, o perché non sai se la chiamata ha bisogno del mutex, allora lo sei trattenendolo troppo a lungo. Stai mirando un fucile da caccia alla tua applicazione e premendo il grilletto. Presumibilmente hai iniziato a usare i thread per ottenere concorrenza; ma hai appena prevenuto la concorrenza. "

  

Il modello mentale giusto per l'uso   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.

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.

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 utilizzano altri metodi (ad 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 qualsiasi altro caso (risolvere solo codici errati, usarlo anche in oggetti diversi) è chiaramente sbagliato!

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top