Articles

Spinlock vs Semaphore

Een spinlock is een lock, en dus een wederzijds uitsluiting (strikt 1 op 1) mechanisme. Het werkt door herhaaldelijk een geheugenlokatie op te vragen en/of te wijzigen, meestal op een atomaire manier. Dit betekent dat het verkrijgen van een spinlock een “drukke” operatie is die mogelijk CPU cycli verbrandt voor een lange tijd (misschien wel voor altijd!) terwijl het effectief “niets” bereikt.
De belangrijkste drijfveer voor zo’n aanpak is het feit dat een context switch een overhead heeft die gelijk is aan een paar honderd (of misschien wel duizend) keer ronddraaien, dus als een slot kan worden verkregen door een paar cycli rond te draaien, kan dit over het algemeen heel goed efficiënter zijn. Ook is het voor realtime toepassingen misschien niet acceptabel om te blokkeren en te wachten tot de planner op een ver tijdstip in de toekomst terugkomt.

#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();

Een semafoor daarentegen draait helemaal niet, of draait maar heel kort (als een optimalisatie om de syscall overhead te vermijden). Als een semafoor niet verworven kan worden, blokkeert hij, en geeft CPU tijd aan een andere thread die klaar is om te draaien. Dit kan natuurlijk betekenen dat er een paar milliseconden voorbijgaan voordat je thread weer ingepland wordt, maar als dit geen probleem is (meestal is dat niet zo) dan kan het een zeer efficiënte, CPU-conservatieve aanpak zijn.

Een goed ontworpen systeem heeft normaal gesproken weinig of geen congestie (dit betekent dat niet alle threads op precies hetzelfde moment proberen het slot te verwerven). Je zou bijvoorbeeld normaal gesproken geen code schrijven die een “lock” verwerft, vervolgens een halve megabyte aan zip-gecomprimeerde gegevens van het netwerk laadt, de gegevens decodeert en parseert, en tenslotte een gedeelde referentie wijzigt (gegevens aan een container toevoegt, enz.) voordat het “lock” wordt vrijgegeven. In plaats daarvan zou men de “lock” alleen verkrijgen om toegang te krijgen tot de gedeelde bron.
Omdat dit betekent dat er aanzienlijk meer werk buiten de kritieke sectie is dan erbinnen, is de kans dat een thread zich binnen de kritieke sectie bevindt natuurlijk relatief laag, en dus zijn er maar weinig threads die tegelijkertijd om de “lock” strijden. Natuurlijk zullen er af en toe twee threads tegelijk proberen het slot te bemachtigen (als dit niet kon gebeuren zou je geen slot nodig hebben!), maar dit is eerder uitzondering dan regel in een “gezond” systeem.

In zo’n geval presteert een spinlock veel beter dan een semafoor, want als er geen lock congestie is, is de overhead van het verkrijgen van de spinlock slechts een tiental cycli in vergelijking met honderden/duizenden cycli voor een context switch of 10-20 miljoen cycli voor het verliezen van de rest van een time slice.

Aan de andere kant, als er veel congestie is, of als de lock voor lange periodes wordt vastgehouden (soms kun je er gewoon niets aan doen!), zal een spinlock krankzinnige hoeveelheden CPU cycli verbranden voor het bereiken van niets.
Een semafoor (of mutex) is een veel betere keuze in dit geval, omdat het een andere thread toestaat om nuttige taken uit te voeren gedurende die tijd. Of, als geen andere thread iets nuttigs te doen heeft, stelt het het besturingssysteem in staat de CPU af te remmen en warmte te verminderen / energie te besparen.