Articles

Spinlock vs. Semaphore

Ein Spinlock ist ein Lock und damit ein Mechanismus des gegenseitigen Ausschlusses (streng 1 zu 1). Er funktioniert durch wiederholtes Abfragen und/oder Ändern einer Speicherstelle, in der Regel auf atomare Weise. Das bedeutet, dass der Erwerb eines Spinlocks eine „arbeitsintensive“ Operation ist, die möglicherweise lange Zeit (vielleicht für immer!) CPU-Zyklen verbrennt, während sie effektiv „nichts“ erreicht.
Der Hauptanreiz für einen solchen Ansatz ist die Tatsache, dass ein Kontextwechsel einen Overhead hat, der einem Spinning von einigen hundert (oder vielleicht tausend) Mal entspricht; wenn also eine Sperre erworben werden kann, indem ein paar Zyklen Spinning verbrannt werden, kann dies insgesamt sehr wohl effizienter sein. Außerdem kann es für Echtzeitanwendungen nicht akzeptabel sein, zu blockieren und darauf zu warten, dass der Scheduler zu einem weit entfernten Zeitpunkt in der Zukunft auf sie zurückkommt.

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

Eine Semaphore hingegen dreht sich entweder überhaupt nicht oder nur für eine sehr kurze Zeit (als Optimierung, um den Syscall-Overhead zu vermeiden). Wenn eine Semaphore nicht erfasst werden kann, blockiert sie und überlässt die CPU-Zeit einem anderen Thread, der zur Ausführung bereit ist. Dies kann natürlich bedeuten, daß einige Millisekunden vergehen, bevor Ihr Thread wieder eingeplant wird, aber wenn dies kein Problem darstellt (normalerweise nicht), dann kann dies ein sehr effizienter, CPU-schonender Ansatz sein.

Ein gut konzipiertes System hat normalerweise geringe oder keine Überlastung (das bedeutet, daß nicht alle Threads versuchen, die Sperre genau zur gleichen Zeit zu erwerben). Beispielsweise würde man normalerweise keinen Code schreiben, der eine Sperre erwirbt, dann ein halbes Megabyte zip-komprimierter Daten aus dem Netz lädt, die Daten dekodiert und parst und schließlich einen gemeinsamen Verweis modifiziert (Daten an einen Container anhängt usw.), bevor er die Sperre freigibt. Da dies bedeutet, daß außerhalb des kritischen Abschnitts wesentlich mehr Arbeit anfällt als innerhalb, ist die Wahrscheinlichkeit, daß sich ein Thread innerhalb des kritischen Abschnitts befindet, natürlich relativ gering, so daß nur wenige Threads gleichzeitig um die Sperre konkurrieren. Natürlich werden hin und wieder zwei Threads gleichzeitig versuchen, die Sperre zu erlangen (wenn das nicht passieren könnte, bräuchte man keine Sperre!), aber das ist in einem „gesunden“ System eher die Ausnahme als die Regel.

In einem solchen Fall ist ein Spinlock einer Semaphore weit überlegen, denn wenn es keine Sperrenüberlastung gibt, beträgt der Overhead für die Beschaffung des Spinlocks nur ein Dutzend Zyklen, verglichen mit Hunderten/Tausenden von Zyklen für einen Kontextwechsel oder 10-20 Millionen Zyklen für den Verlust des Rests einer Zeitscheibe.

Andererseits, bei hoher Auslastung oder wenn die Sperre über längere Zeiträume gehalten wird (manchmal kann man einfach nicht anders!), verbrennt ein Spinlock irrsinnige Mengen an CPU-Zyklen für nichts.
Ein Semaphor (oder Mutex) ist in diesem Fall eine viel bessere Wahl, da es einem anderen Thread erlaubt, während dieser Zeit nützliche Aufgaben auszuführen. Oder, wenn kein anderer Thread etwas Nützliches zu tun hat, erlaubt es dem Betriebssystem, die CPU zu drosseln und Wärme zu reduzieren / Energie zu sparen.