Articles

Spinlock vs Semaphore

En spinlock är en låsning och därför en mekanism för ömsesidig uteslutning (strikt 1 till 1). Den fungerar genom att upprepade gånger fråga och/eller ändra en minnesplats, vanligtvis på ett atomärt sätt. Detta innebär att förvärva en spinlock är en ”upptagen” operation som eventuellt bränner CPU-cykler under lång tid (kanske för evigt!) samtidigt som den i praktiken uppnår ”ingenting”.
Det främsta incitamentet för ett sådant tillvägagångssätt är det faktum att en kontextomkoppling har en overhead som motsvarar att snurra några hundra (eller kanske tusen) gånger, så om en spinnlock kan förvärvas genom att bränna några cykler genom att snurra, kan detta totalt sett mycket väl vara mer effektivt. För realtidstillämpningar kanske det inte heller är acceptabelt att blockera och vänta på att schemaläggaren ska komma tillbaka till dem vid någon avlägsen tidpunkt i framtiden.

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

En semafor däremot snurrar antingen inte alls, eller så snurrar den bara under en mycket kort tid (som en optimering för att undvika överkostnaden för syscall). Om en semafor inte kan förvärvas blockeras den och ger CPU-tid till en annan tråd som är redo att köras. Detta kan naturligtvis innebära att det går några millisekunder innan din tråd schemaläggs igen, men om detta inte är något problem (vilket det vanligtvis inte är) så kan det vara ett mycket effektivt, CPU-konservativt tillvägagångssätt.

Ett väldesignat system har normalt sett låg eller ingen överbelastning (detta innebär att alla trådar inte försöker förvärva spärren vid exakt samma tidpunkt). Man skulle till exempel normalt inte skriva kod som förvärvar en spärr, sedan laddar en halv megabyte zip-komprimerad data från nätverket, avkodar och analyserar data och slutligen ändrar en delad referens (lägger till data till en behållare osv.) innan spärren släpps. Istället skulle man förvärva låset endast för att få tillgång till den delade resursen.
Då detta innebär att det finns betydligt mer arbete utanför den kritiska sektionen än innanför den, är naturligtvis sannolikheten för att en tråd befinner sig innanför den kritiska sektionen relativt låg, och därmed är det få trådar som konkurrerar om låset samtidigt. Naturligtvis kommer då och då två trådar att försöka förvärva spärren samtidigt (om detta inte kunde hända skulle man inte behöva någon spärr!), men detta är snarare undantaget än regeln i ett ”friskt” system.

I ett sådant fall är en spinlock mycket effektivare än en semafor, eftersom om det inte finns någon överbelastning av låsen, är omkostnaden för att förvärva spinlocket endast ett dussintal cykler jämfört med hundratals/tusentals cykler för ett kontextbyte eller 10-20 miljoner cykler för att förlora återstoden av en tidsdel.

Å andra sidan, vid hög överbelastning, eller om låset hålls kvar under långa perioder (ibland kan man bara inte låta bli!), kommer en spinlock att bränna vansinnigt många CPU-cykler för att inte åstadkomma någonting.
En semafor (eller mutex) är ett mycket bättre val i det här fallet, eftersom den tillåter en annan tråd att köra användbara uppgifter under den tiden. Eller, om ingen annan tråd har något användbart att göra, gör den det möjligt för operativsystemet att strypa CPU:n och minska värmen/behålla energin.