Risposte:
Le serrature vengono utilizzate per l'esclusione reciproca. Quando vuoi assicurarti che un pezzo di codice sia atomico, mettici un lucchetto intorno. In teoria potresti usare un semaforo binario per farlo, ma questo è un caso speciale.
I semafori e le variabili di condizione si basano sull'esclusione reciproca fornita dai blocchi e vengono utilizzati per fornire un accesso sincronizzato alle risorse condivise. Possono essere utilizzati per scopi simili.
Una variabile di condizione viene generalmente utilizzata per evitare un'attesa occupata (ripetutamente ripetuta durante il controllo di una condizione) durante l'attesa che una risorsa diventi disponibile. Ad esempio, se hai un thread (o più thread) che non può continuare fino a quando una coda non è vuota, l'approccio di attesa occupata sarebbe semplicemente fare qualcosa come:
//pseudocode
while(!queue.empty())
{
sleep(1);
}
Il problema con questo è che stai sprecando tempo del processore facendo controllare ripetutamente la condizione da questo thread. Perché invece non avere una variabile di sincronizzazione che può essere segnalata per dire al thread che la risorsa è disponibile?
//pseudocode
syncVar.lock.acquire();
while(!queue.empty())
{
syncVar.wait();
}
//do stuff with queue
syncVar.lock.release();
Presumibilmente, avrai un thread da qualche altra parte che sta tirando le cose fuori dalla coda. Quando la coda è vuota, può chiamare syncVar.signal()
per svegliare un thread casuale su cui è seduto addormentato syncVar.wait()
(o di solito c'è anche un metodo signalAll()
o broadcast()
per svegliare tutti i thread che sono in attesa).
In genere uso variabili di sincronizzazione come questa quando ho uno o più thread in attesa di una singola condizione particolare (ad esempio, che la coda sia vuota).
I semafori possono essere usati in modo simile, ma penso che siano usati meglio quando hai una risorsa condivisa che può essere disponibile e non disponibile in base a un numero intero di cose disponibili. I semafori sono utili per le situazioni produttore / consumatore in cui i produttori allocano risorse e i consumatori le consumano.
Pensa se avessi un distributore automatico di bibite. C'è solo una macchina per bibite ed è una risorsa condivisa. Hai un thread che è un venditore (produttore) che è responsabile di mantenere la macchina rifornita e N thread che sono acquirenti (consumatori) che vogliono ottenere bibite dalla macchina. Il numero di bibite nella macchina è il valore intero che guiderà il nostro semaforo.
Ogni thread acquirente (consumatore) che arriva alla macchina della soda chiama il down()
metodo del semaforo per prendere una soda. Questo prenderà una soda dalla macchina e decrementerà il conteggio delle bibite disponibili di 1. Se sono disponibili bibite, il codice continuerà a scorrere oltre l' down()
istruzione senza problemi. Se non sono disponibili bibite gassate, il thread dormirà qui in attesa di essere avvisato di quando la soda sarà nuovamente disponibile (quando ci sono più bibite nella macchina).
Il thread del venditore (produttore) starebbe essenzialmente aspettando che il distributore di bibite sia vuoto. Il venditore viene avvisato quando l'ultima soda viene prelevata dalla macchina (e uno o più consumatori sono potenzialmente in attesa di estrarre le bibite). Il venditore rifornirebbe la macchina della soda con il up()
metodo del semaforo , il numero disponibile di bibite verrebbe incrementato ogni volta e quindi i thread dei consumatori in attesa riceverebbero una notifica che è disponibile più soda.
I metodi wait()
e signal()
di una variabile di sincronizzazione tendono a essere nascosti all'interno delle operazioni down()
e up()
del semaforo.
Certamente c'è una sovrapposizione tra le due scelte. Esistono molti scenari in cui un semaforo o una variabile di condizione (o un insieme di variabili di condizione) potrebbero entrambi servire ai tuoi scopi. Sia i semafori che le variabili di condizione sono associati a un oggetto lock che usano per mantenere l'esclusione reciproca, ma poi forniscono funzionalità extra oltre al lock per sincronizzare l'esecuzione del thread. Spetta principalmente a te capire quale ha più senso per la tua situazione.
Non è necessariamente la descrizione più tecnica, ma è così che ha senso nella mia testa.
Riveliamo cosa c'è sotto il cofano.
La variabile condizionale è essenzialmente una coda di attesa , che supporta le operazioni di attesa di blocco e di riattivazione, ovvero è possibile inserire un thread nella coda di attesa e impostare il suo stato su BLOCK, quindi estrarne un thread e impostarne lo stato su READY.
Nota che per usare una variabile condizionale, sono necessari altri due elementi:
Il protocollo quindi diventa,
Il semaforo è essenzialmente un contatore + un mutex + una coda di attesa. E può essere utilizzato così com'è senza dipendenze esterne. Puoi usarlo come mutex o come variabile condizionale.
Pertanto, il semaforo può essere trattato come una struttura più sofisticata rispetto alla variabile condizionale, mentre quest'ultima è più leggera e flessibile.
I semafori possono essere utilizzati per implementare l'accesso esclusivo alle variabili, tuttavia devono essere utilizzati per la sincronizzazione. I mutex, invece, hanno una semantica strettamente correlata alla mutua esclusione: solo il processo che ha bloccato la risorsa può sbloccarla.
Sfortunatamente non puoi implementare la sincronizzazione con i mutex, ecco perché abbiamo variabili di condizione. Si noti inoltre che con le variabili di condizione è possibile sbloccare tutti i thread in attesa nello stesso istante utilizzando lo sblocco della trasmissione. Questo non può essere fatto con i semafori.
le variabili semaforo e condizione sono molto simili e vengono utilizzate principalmente per gli stessi scopi. Tuttavia, ci sono piccole differenze che potrebbero renderne uno preferibile. Ad esempio, per implementare la sincronizzazione della barriera non saresti in grado di utilizzare un semaforo, ma una variabile di condizione è l'ideale.
La sincronizzazione della barriera è quando vuoi che tutti i tuoi thread aspettino fino a quando tutti sono arrivati a una certa parte nella funzione thread. questo può essere implementato avendo una variabile statica che è inizialmente il valore dei thread totali decrementato da ogni thread quando raggiunge quella barriera. questo significherebbe che vogliamo che ogni thread dorma fino all'arrivo dell'ultimo, un semaforo farebbe l'esatto contrario! con un semaforo, ogni thread continuerà a funzionare e l'ultimo thread (che imposterà il valore del semaforo a 0) andrà a dormire.
una condizione variabile d'altra parte, è l'ideale. quando ogni thread arriva alla barriera controlliamo se il nostro contatore statico è zero. in caso contrario, impostiamo il thread in sleep con la funzione wait della variabile di condizione. quando l'ultimo thread arriva alla barriera, il valore del contatore verrà decrementato a zero e quest'ultimo chiamerà la funzione segnale della variabile di condizione che sveglierà tutti gli altri thread!
File le variabili di condizione sotto la sincronizzazione del monitor. In genere ho visto semafori e monitor come due diversi stili di sincronizzazione. Ci sono differenze tra i due in termini di quantità di dati di stato intrinsecamente conservati e di come si desidera modellare il codice, ma in realtà non c'è alcun problema che possa essere risolto da uno ma non dall'altro.
Tendo a codificare per monitorare la forma; nella maggior parte dei linguaggi in cui lavoro si tratta di mutex, variabili di condizione e alcune variabili di stato di supporto. Ma anche i semafori farebbero il lavoro.
I mutex
e conditional variables
sono ereditati da semaphore
.
mutex
, semaphore
utilizza due stati: 0, 1condition variables
gli semaphore
usi da banco.Sono come lo zucchero sintattico