Spinlock e semaforo differiscono principalmente in quattro cose:
1. Cosa sono
Uno spinlock è una possibile implementazione di un blocco, vale a dire uno che viene implementato dall'attesa occupata ("rotazione"). Un semaforo è una generalizzazione di un blocco (o, viceversa, un blocco è un caso speciale di un semaforo). Di solito, ma non necessariamente , gli spinlock sono validi solo all'interno di un processo, mentre i semafori possono essere utilizzati anche per la sincronizzazione tra processi diversi.
Un lucchetto funziona per mutua esclusione, cioè uno thread alla volta può acquisire il lock e procedere con una "sezione critica" di codice. Di solito, questo significa codice che modifica alcuni dati condivisi da più thread.
Un semaforo ha un contatore e si lascerà acquisire da uno o più thread, a seconda del valore che inserisci e (in alcune implementazioni) a seconda del valore massimo consentito.
Pertanto, si può considerare un lock un caso particolare di un semaforo con valore massimo pari a 1.
2. Cosa fanno
Come affermato sopra, uno spinlock è un blocco, e quindi un meccanismo di mutua esclusione (rigorosamente 1 a 1). Funziona interrogando ripetutamente e/o modificando una posizione di memoria, di solito in modo atomico. Ciò significa che l'acquisizione di uno spinlock è un'operazione "occupata" che probabilmente brucia i cicli della CPU per molto tempo (forse per sempre!) Mentre effettivamente non ottiene "nulla".
L'incentivo principale per un tale approccio è il fatto che un cambio di contesto ha un sovraccarico equivalente alla rotazione di alcune centinaia (o forse migliaia) di volte, quindi se un blocco può essere acquisito bruciando alcuni cicli di rotazione, questo potrebbe nel complesso benissimo essere più efficiente. Inoltre, per le applicazioni in tempo reale potrebbe non essere accettabile bloccare e attendere che lo scheduler torni da loro in un momento lontano nel futuro.
Un semaforo, al contrario, non gira affatto o gira solo per un tempo molto breve (come ottimizzazione per evitare l'overhead della chiamata di sistema). Se un semaforo non può essere acquisito, si blocca, cedendo il tempo della CPU a un thread diverso pronto per l'esecuzione. Ciò può ovviamente significare che passano alcuni millisecondi prima che il tuo thread venga programmato di nuovo, ma se questo non è un problema (di solito non lo è), allora può essere un approccio molto efficiente e conservativo della CPU.
3. Come si comportano in presenza di congestione
È un'idea sbagliata comune che gli spinlock o gli algoritmi senza blocco siano "generalmente più veloci" o che siano utili solo per "compiti molto brevi" (idealmente, nessun oggetto di sincronizzazione dovrebbe essere tenuto più a lungo di quanto assolutamente necessario, mai).
L'unica differenza importante è il modo in cui i diversi approcci si comportano in presenza di congestione .
Un sistema ben progettato normalmente ha una congestione bassa o nulla (questo significa che non tutti i thread tentano di acquisire il blocco nello stesso momento). Ad esempio, normalmente non scrivere codice che acquisisce un blocco, quindi carica mezzo megabyte di dati compressi zip dalla rete, decodifica e analizza i dati e infine modifica un riferimento condiviso (aggiungi dati a un contenitore, ecc.) prima di rilasciare il blocco. Invece, si acquisirebbe il blocco solo allo scopo di accedere alla risorsa condivisa .
Poiché questo significa che c'è molto più lavoro al di fuori della sezione critica che al suo interno, naturalmente la probabilità che un thread si trovi all'interno della sezione critica è relativamente bassa, e quindi pochi thread si contendono il lock allo stesso tempo. Ovviamente ogni tanto due thread cercheranno di acquisire il blocco contemporaneamente (se non fosse possibile capita che non avresti bisogno di un lucchetto!), ma questa è piuttosto l'eccezione che la regola in un sistema "sano".
In tal caso, uno spinlock grande supera un semaforo perché se non c'è congestione del blocco, il sovraccarico dell'acquisizione dello spinlock è di appena una dozzina di cicli rispetto a centinaia/migliaia di cicli per un cambio di contesto o 10-20 milioni di cicli per perdere il resto di un intervallo di tempo.
D'altra parte, data un'elevata congestione, o se il blocco viene mantenuto per lunghi periodi (a volte non puoi farne a meno!), Uno spinlock brucerà quantità folli di cicli della CPU per non ottenere nulla.
Un semaforo (o mutex) è una scelta molto migliore in questo caso, in quanto consente l'esecuzione di un thread diverso utile compiti durante quel periodo. Oppure, se nessun altro thread ha qualcosa di utile da fare, consente al sistema operativo di rallentare la CPU e ridurre il calore/risparmiare energia.
Inoltre, su un sistema single-core, uno spinlock sarà piuttosto inefficiente in presenza di congestione del blocco, poiché un thread in rotazione perderà tutto il suo tempo in attesa di un cambiamento di stato che non può assolutamente avvenire (non fino a quando il thread di rilascio non è pianificato, il che non sta accadendo mentre il thread in attesa è in esecuzione!). Pertanto, dato qualsiasi quantità di contesa, l'acquisizione del blocco richiede circa 1 ½ intervallo di tempo nel migliore dei casi (supponendo che il thread di rilascio sia il successivo in fase di pianificazione), il che non è un comportamento molto positivo.
4. Come vengono implementati
Un semaforo al giorno d'oggi avvolgerà tipicamente sys_futex
sotto Linux (facoltativamente con uno spinlock che esce dopo alcuni tentativi).
Uno spinlock viene in genere implementato utilizzando operazioni atomiche e senza utilizzare nulla fornito dal sistema operativo. In passato, ciò significava utilizzare gli elementi intrinseci del compilatore o le istruzioni dell'assemblatore non portatile. Nel frattempo, sia C++11 che C11 hanno operazioni atomiche come parte del linguaggio, quindi a parte la difficoltà generale di scrivere codice lock-free dimostrabilmente corretto, è ora possibile implementare codice lock-free in un modo interamente portabile e (quasi) modo indolore.
Oltre a quanto detto da Yoav Aviram e gbjbaanb, l'altro punto chiave era che non avresti mai usato uno spin-lock su una macchina a CPU singola, mentre un semaforo avrebbe avuto senso su una macchina del genere. Al giorno d'oggi, è spesso difficile trovare una macchina senza più core, o hyperthreading o equivalente, ma nelle circostanze in cui si dispone di una sola CPU, è necessario utilizzare i semafori. (Confido che il motivo sia ovvio. Se la singola CPU è occupata in attesa che qualcos'altro rilasci lo spin-lock, ma è in esecuzione sull'unica CPU, è improbabile che il blocco venga rilasciato fino a quando il processo o il thread corrente non viene annullato da il sistema operativo, che potrebbe richiedere del tempo e non accade nulla di utile fino a quando non si verifica la prelazione.)
molto semplicemente, un semaforo è un oggetto di sincronizzazione "cedevole", uno spinlock è uno "busywait". (c'è qualcosa in più nei semafori in quanto sincronizzano diversi thread, a differenza di un mutex o guard o monitor o sezione critica che protegge una regione di codice da un singolo thread)
Useresti un semaforo in più circostanze, ma usi uno spinlock in cui ti bloccherai per un tempo molto breve:c'è un costo per il blocco, specialmente se blocchi molto. In questi casi può essere più efficiente eseguire lo spinlock per un po' mentre si attende che la risorsa protetta venga sbloccata. Ovviamente c'è un calo delle prestazioni se giri troppo a lungo.
in genere se giri per più di un quanto di thread, allora dovresti usare un semaforo.