Mentre un mutex può essere usato per risolvere altri problemi, il motivo principale per cui esistono è quello di fornire l'esclusione reciproca e quindi risolvere quella che è conosciuta come una condizione di razza. Quando due (o più) thread o processi stanno tentando di accedere contemporaneamente alla stessa variabile, abbiamo il potenziale per una condizione di competizione. Considera il seguente codice
//somewhere long ago, we have i declared as int
void my_concurrently_called_function()
{
i++;
}
Gli interni di questa funzione sembrano così semplici. È solo un'affermazione. Tuttavia, un tipico equivalente del linguaggio pseudoassemblaggio potrebbe essere:
load i from memory into a register
add 1 to i
store i back into memory
Poiché per eseguire l'operazione di incremento su i sono necessarie tutte le istruzioni equivalenti in linguaggio assembly, diciamo che incrementare i è un'operazione non atmoica. Un'operazione atomica può essere completata sull'hardware con una garanzia di non essere interrotta una volta iniziata l'esecuzione dell'istruzione. L'aumento i consiste in una catena di 3 istruzioni atomiche. In un sistema concorrente in cui più thread chiamano la funzione, sorgono problemi quando un thread legge o scrive nel momento sbagliato. Immagina di avere due thread in esecuzione simultanea e uno chiama la funzione immediatamente dopo l'altro. Diciamo anche che abbiamo inizializzato a 0. Supponiamo anche che abbiamo molti registri e che i due thread utilizzino registri completamente diversi, quindi non ci saranno collisioni. I tempi effettivi di questi eventi possono essere:
thread 1 load 0 into register from memory corresponding to i //register is currently 0
thread 1 add 1 to a register //register is now 1, but not memory is 0
thread 2 load 0 into register from memory corresponding to i
thread 2 add 1 to a register //register is now 1, but not memory is 0
thread 1 write register to memory //memory is now 1
thread 2 write register to memory //memory is now 1
Quello che è successo è che abbiamo due thread che incrementano contemporaneamente, la nostra funzione viene chiamata due volte, ma il risultato non è coerente con questo fatto. Sembra che la funzione sia stata chiamata una sola volta. Questo perché l'atomicità è "rotta" a livello di macchina, il che significa che i thread possono interrompersi a vicenda o lavorare insieme nei momenti sbagliati.
Abbiamo bisogno di un meccanismo per risolvere questo. Dobbiamo imporre alcuni ordini alle istruzioni sopra. Un meccanismo comune è quello di bloccare tutti i thread tranne uno. Pthread mutex utilizza questo meccanismo.
Qualsiasi thread che deve eseguire alcune righe di codice che possono modificare in modo non sicuro valori condivisi da altri thread contemporaneamente (usando il telefono per parlare con sua moglie), dovrebbe prima essere fatto acquisire un blocco su un mutex. In questo modo, qualsiasi thread che richiede l'accesso ai dati condivisi deve passare attraverso il blocco mutex. Solo allora un thread sarà in grado di eseguire il codice. Questa sezione del codice è chiamata sezione critica.
Una volta eseguita la sezione critica, il thread dovrebbe rilasciare il blocco sul mutex in modo che un altro thread possa acquisire un blocco sul mutex.
Il concetto di avere un mutex sembra un po 'strano quando si considerano gli umani che cercano un accesso esclusivo a oggetti fisici reali, ma durante la programmazione, dobbiamo essere intenzionali. Discussioni e processi concorrenti non hanno l'educazione sociale e culturale che facciamo, quindi dobbiamo costringerli a condividere i dati in modo corretto.
Tecnicamente parlando, come funziona un mutex? Non soffre delle stesse condizioni di gara che abbiamo menzionato prima? Pthread_mutex_lock () non è forse un po 'più complesso di un semplice incremento di una variabile?
Tecnicamente parlando, abbiamo bisogno di supporto hardware per aiutarci. I progettisti hardware ci danno istruzioni sulla macchina che fanno più di una cosa ma sono garantite per essere atomiche. Un classico esempio di tale istruzione è il test-and-set (TAS). Quando si tenta di acquisire un blocco su una risorsa, è possibile che TAS verifichi se un valore in memoria è 0. In caso affermativo, questo sarebbe il nostro segnale che la risorsa è in uso e non facciamo nulla (o più accuratamente , attendiamo un meccanismo. Un mutex di pthreads ci metterà in una coda speciale nel sistema operativo e ci avviserà quando la risorsa sarà disponibile. I sistemi più stupidi potrebbero richiedere di eseguire un loop spin stretto, testando la condizione più e più volte) . Se il valore in memoria non è 0, TAS imposta la posizione su un valore diverso da 0 senza utilizzare altre istruzioni. E' è come combinare due istruzioni di assemblaggio in 1 per darci atomicità. Pertanto, una volta iniziato, il test e la modifica del valore (se la modifica è appropriata) non possono essere interrotti. Possiamo costruire mutex sopra una simile istruzione.
Nota: alcune sezioni potrebbero apparire simili a una risposta precedente. Ho accettato il suo invito a modificare, ha preferito l'originale, quindi sto mantenendo ciò che avevo che è infuso con un po 'della sua verbosità.