Articles

Spinlock vs Semaphore

Uno spinlock è un blocco, e quindi un meccanismo di mutua esclusione (rigorosamente 1 a 1). Funziona interrogando e/o modificando ripetutamente una posizione di memoria, di solito in modo atomico. Questo significa che l’acquisizione di uno spinlock è un’operazione “impegnata” che possibilmente brucia cicli di CPU per molto tempo (forse per sempre!) mentre effettivamente non ottiene “nulla”.
Il principale incentivo per un tale approccio è il fatto che un context switch ha un overhead equivalente a una rotazione di alcune centinaia (o forse migliaia) di volte, quindi se un lock può essere acquisito bruciando alcuni cicli di rotazione, questo può essere complessivamente molto più efficiente. Inoltre, per applicazioni in tempo reale potrebbe non essere accettabile bloccarsi e aspettare che lo scheduler torni da loro in qualche momento lontano nel futuro.

#include <boost/atomic.hpp>
class spinlock {
private:
typedef enum {Locked, Unlocked} LockState;
boost::atomic<LockState> state_;
public:
spinlock() : state_(Unlocked) {}
void lock()
{
while (state_.exchange(Locked, boost::memory_order_acquire) == Locked) {
/* busy-wait */
}
}
void unlock()
{
state_.store(Unlocked, boost::memory_order_release);
}
};// Usagespinlock s;
s.lock();
// access data structure here
s.unlock();

Un semaforo, al contrario, o non gira affatto, o gira solo per un tempo molto breve (come ottimizzazione per evitare l’overhead della syscall). Se un semaforo non può essere acquisito, si blocca, cedendo il tempo della CPU ad un altro thread che è pronto a girare. Questo può naturalmente significare che passano alcuni millisecondi prima che il vostro thread sia programmato di nuovo, ma se questo non è un problema (di solito non lo è) allora può essere un approccio molto efficiente e conservativo per la CPU.

Un sistema ben progettato normalmente ha una bassa o nessuna congestione (questo significa che non tutti i thread cercano di acquisire il lock nello stesso esatto momento). Per esempio, normalmente non si dovrebbe scrivere codice che acquisisce un blocco, poi carica mezzo megabyte di dati compressi dalla rete, decodifica e analizza i dati, e infine modifica un riferimento condiviso (aggiunge dati a un contenitore, ecc.) prima di rilasciare il blocco. Invece, si acquisirebbe il lock solo allo scopo di accedere alla risorsa condivisa.
Siccome questo significa che c’è molto più lavoro fuori dalla sezione critica che dentro, naturalmente la probabilità che un thread sia dentro la sezione critica è relativamente bassa, e quindi pochi thread si contendono il lock allo stesso tempo. Naturalmente ogni tanto due thread cercheranno di acquisire il lock allo stesso tempo (se questo non potesse accadere non avreste bisogno di un lock!), ma questa è piuttosto l’eccezione che la regola in un sistema “sano”.

In tal caso, uno spinlock supera di gran lunga un semaforo, perché se non c’è congestione del lock, l’overhead di acquisizione dello spinlock è una semplice dozzina di cicli rispetto alle centinaia/migliaia di cicli per un context switch o ai 10-20 milioni di cicli per perdere il resto di una fetta di tempo.

D’altra parte, data l’alta congestione, o se il blocco viene tenuto per lunghi periodi (a volte non si può proprio farne a meno!), uno spinlock brucerà quantità folli di cicli di CPU per non ottenere nulla.
Un semaforo (o mutex) è una scelta molto migliore in questo caso, in quanto permette ad un thread diverso di eseguire compiti utili durante quel tempo. Oppure, se nessun altro thread ha qualcosa di utile da fare, permette al sistema operativo di rallentare la CPU e ridurre il calore / conservare l’energia.

.