Articles

Spinlock vs. Semaphore

A spinlock egy lock, tehát egy kölcsönös kizárási (szigorúan 1-1) mechanizmus. Úgy működik, hogy ismételten lekérdez és/vagy módosít egy memóriahelyet, általában atomi módon. Ez azt jelenti, hogy egy spinlock megszerzése egy “elfoglalt” művelet, amely esetleg hosszú ideig (talán örökké!) CPU-ciklusokat éget, miközben gyakorlatilag “semmit” nem ér el.
Az ilyen megközelítés fő ösztönzője az a tény, hogy egy kontextusváltás néhány száz (vagy talán ezer) pörgetéssel egyenértékű többletköltséggel jár, így ha egy lockot néhány ciklus pörgetéssel történő elégetésével lehet megszerezni, ez összességében nagyon is hatékonyabb lehet. Valamint a valós idejű alkalmazások számára nem biztos, hogy elfogadható, ha blokkolni kell, és várni, hogy az ütemező egy távoli jövőbeni időpontban visszatérjen hozzájuk.

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

A szemafor ezzel szemben vagy egyáltalán nem pörög, vagy csak nagyon rövid ideig pörög (optimalizálásként, hogy elkerüljük a syscall overheadet). Ha egy szemafor nem szerezhető meg, akkor blokkol, és CPU-időt ad át egy másik futásra kész szálnak. Ez persze azt is jelentheti, hogy néhány ezredmásodperc telik el, mire a szálunk újra be van ütemezve, de ha ez nem jelent problémát (általában nem), akkor ez egy nagyon hatékony, CPU-kímélő megközelítés lehet.

Egy jól megtervezett rendszerben általában alacsony vagy egyáltalán nincs torlódás (ez azt jelenti, hogy nem minden szál pontosan ugyanabban az időben próbálja megszerezni a zárat). Normális esetben például nem írnánk olyan kódot, amely megszerzi a zárat, majd fél megabájtnyi zip-tömörített adatot tölt be a hálózatról, dekódolja és elemzi az adatokat, végül módosít egy megosztott hivatkozást (adatot csatol egy tárolóhoz stb.), mielőtt feloldaná a zárat. Ehelyett csak a megosztott erőforrás eléréséhez szereznénk meg a zárat.
Mivel ez azt jelenti, hogy a kritikus szakaszon kívül lényegesen több munka van, mint azon belül, természetesen viszonylag kicsi a valószínűsége annak, hogy egy szál a kritikus szakaszon belül van, és így kevés szál verseng egyszerre a zárért. Persze időnként előfordul, hogy két szál egyszerre próbálja megszerezni a zárat (ha ez nem történhetne meg, akkor nem lenne szükség zárra!), de ez egy “egészséges” rendszerben inkább a kivétel, mint a szabály.

Egy ilyen esetben a spinlock jelentősen felülmúlja a semaphore-t, mert ha nincs lock torlódás, akkor a spinlock megszerzésének overheadje mindössze néhány tucat ciklus, szemben a kontextusváltás több száz/ezer ciklusával vagy a maradék időszelet elvesztésének 10-20 millió ciklusával.

Másrészt, nagy zsúfoltság esetén, vagy ha a zárat hosszú ideig tartjuk (néha egyszerűen nem tehetünk róla!), a spinlock őrült mennyiségű CPU-ciklust éget el a semmiért.
A szemafor (vagy mutex) sokkal jobb választás ebben az esetben, mivel lehetővé teszi egy másik szál számára, hogy hasznos feladatokat futtasson ez idő alatt. Vagy, ha nincs más szálnak hasznos dolga, akkor lehetővé teszi az operációs rendszer számára, hogy a CPU-t visszafogja és csökkentse a hőt / energiát takarítson meg.