Spinlock und Semaphore unterscheiden sich hauptsächlich in vier Dingen:
1. Was sie sind
Ein Spinlock ist eine mögliche Implementierung einer Sperre, nämlich eine, die durch beschäftigtes Warten ("spinning") implementiert wird. Eine Semaphore ist eine Verallgemeinerung einer Sperre (oder umgekehrt, eine Sperre ist ein Spezialfall einer Semaphore). Normalerweise, aber nicht unbedingt , sind Spinlocks nur innerhalb eines Prozesses gültig, während Semaphore auch zur Synchronisierung zwischen verschiedenen Prozessen verwendet werden können.
Eine Sperre dient zum gegenseitigen Ausschluss, also eine Threads gleichzeitig die Sperre erwerben und mit einem "kritischen Abschnitt" des Codes fortfahren können. Normalerweise bedeutet dies Code, der einige Daten ändert, die von mehreren Threads gemeinsam genutzt werden.
Ein Semaphor hat einen Zähler und lässt sich von einem oder mehreren erwerben Threads abhängig davon, welchen Wert Sie dort posten, und (in einigen Implementierungen) abhängig davon, was sein maximal zulässiger Wert ist.
Insofern kann man eine Sperre als Sonderfall einer Semaphore mit einem Maximalwert von 1 betrachten.
2. Was sie tun
Wie oben erwähnt, ist ein Spinlock eine Sperre und daher ein gegenseitiger Ausschlussmechanismus (ausschließlich 1 zu 1). Es funktioniert durch wiederholtes Abfragen und/oder Modifizieren eines Speicherplatzes, normalerweise auf atomare Weise. Dies bedeutet, dass der Erwerb eines Spinlocks eine "beschäftigte" Operation ist, die möglicherweise CPU-Zyklen für eine lange Zeit (vielleicht für immer!) 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 paar hundert (oder vielleicht tausend) Drehungen entspricht. Wenn also eine Sperre durch das Brennen einiger Zyklen des Drehens erworben werden kann, kann dies insgesamt sehr gut sein effizienter. Außerdem ist es für Echtzeitanwendungen möglicherweise nicht akzeptabel, zu blockieren und darauf zu warten, dass der Scheduler zu einem weit entfernten Zeitpunkt in der Zukunft auf sie zurückkommt.
Im Gegensatz dazu dreht sich eine Semaphore entweder überhaupt nicht oder nur für eine sehr kurze Zeit (als Optimierung, um den Syscall-Overhead zu vermeiden). Wenn ein Semaphor nicht abgerufen werden kann, blockiert es und gibt CPU-Zeit an einen anderen Thread ab, der bereit ist, ausgeführt zu werden. Dies kann natürlich bedeuten, dass ein paar Millisekunden vergehen, bevor Ihr Thread erneut geplant wird, aber wenn dies kein Problem ist (normalerweise ist es das nicht), dann kann es ein sehr effizienter, CPU-schonender Ansatz sein.
3. Wie sie sich bei Staus verhalten
Es ist ein weit verbreiteter Irrglaube, dass Spinlocks oder lockfreie Algorithmen „allgemein schneller“ seien oder dass sie nur für „sehr kurze Aufgaben“ nützlich seien (im Idealfall sollte kein Synchronisationsobjekt länger als unbedingt nötig gehalten werden).
Der einzige wichtige Unterschied besteht darin, wie sich die verschiedenen Ansätze bei Staus verhalten .
Ein gut gestaltetes System hat normalerweise keine oder nur eine geringe Überlastung (das bedeutet, dass nicht alle Threads gleichzeitig versuchen, die Sperre zu erlangen). Zum Beispiel würde man normalerweise nicht Code schreiben, der eine Sperre erwirbt, dann ein halbes Megabyte zip-komprimierter Daten aus dem Netzwerk lädt, die Daten dekodiert und parst und schließlich eine gemeinsame Referenz modifiziert (Daten an einen Container anhängt usw.), bevor die Sperre aufgehoben wird. Stattdessen würde man die Sperre nur zum Zwecke des Zugriffs auf die gemeinsame Ressource erwerben .
Da dies bedeutet, dass außerhalb des kritischen Abschnitts erheblich mehr Arbeit anfällt als innerhalb, ist die Wahrscheinlichkeit, dass sich ein Thread innerhalb des kritischen Abschnitts befindet, natürlich relativ gering, und daher konkurrieren wenige Threads gleichzeitig um die Sperre. Natürlich werden hin und wieder zwei Threads gleichzeitig versuchen, die Sperre zu erlangen (wenn dies nicht konnte Im Notfall bräuchten Sie kein Schloss!), aber das ist in einem "gesunden" System eher die Ausnahme als die Regel.
In einem solchen Fall ist ein Spinlock groß übertrifft ein Semaphor, denn wenn es keine Lock-Überlastung gibt, beträgt der Overhead für den Erwerb des Spinlocks nur ein Dutzend Zyklen im Vergleich zu Hunderten/Tausenden von Zyklen für einen Kontextwechsel oder 10-20 Millionen Zyklen für den Verlust des Rests einer Zeitscheibe.
Auf der anderen Seite wird ein Spinlock bei hoher Überlastung oder wenn die Sperre für längere Zeit gehalten wird (manchmal kann man einfach nicht anders!), wahnsinnige Mengen an CPU-Zyklen verbrauchen, um nichts zu erreichen.
Ein Semaphor (oder Mutex) ist in diesem Fall eine viel bessere Wahl, da es ermöglicht, dass ein anderer Thread nützlich ausgeführt wird Aufgaben in dieser Zeit. 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.
Außerdem ist ein Spinlock auf einem Single-Core-System bei Vorhandensein einer Sperrüberlastung ziemlich ineffizient, da ein sich drehender Thread seine gesamte Zeit damit verschwendet, auf eine Zustandsänderung zu warten, die unmöglich passieren kann (nicht bis der freigebende Thread geplant ist, was passiert nicht während der wartende Thread läuft!). Daher gegeben beliebig Umfang des Konflikts dauert das Erlangen der Sperre im besten Fall etwa 1 1/2 Zeitscheiben (vorausgesetzt, der freigebende Thread ist der nächste, der geplant wird), was kein sehr gutes Verhalten ist.
4. Wie sie implementiert werden
Ein Semaphor umschließt heutzutage typischerweise sys_futex
unter Linux (optional mit einem Spinlock, der nach einigen Versuchen beendet wird).
Ein Spinlock wird normalerweise unter Verwendung von atomaren Operationen und ohne Verwendung von irgendetwas, das vom Betriebssystem bereitgestellt wird, implementiert. In der Vergangenheit bedeutete dies, entweder Compiler-Intrinsics oder nicht-portable Assembler-Anweisungen zu verwenden. Mittlerweile haben sowohl C++11 als auch C11 atomare Operationen als Teil der Sprache, sodass es, abgesehen von der allgemeinen Schwierigkeit, nachweislich korrekten lock-freien Code zu schreiben, jetzt möglich ist, lock-freien Code in einem vollständig portablen und (fast) schmerzlose Weise.
Abgesehen von dem, was Yoav Aviram und gbjbaanb gesagt haben, war der andere wichtige Punkt früher, dass Sie niemals ein Spin-Lock auf einem Computer mit einer CPU verwenden würden, während ein Semaphor auf einem solchen Computer sinnvoll wäre. Heutzutage ist es häufig schwierig, eine Maschine ohne mehrere Kerne oder Hyperthreading oder Ähnliches zu finden, aber unter den Umständen, dass Sie nur eine einzige CPU haben, sollten Sie Semaphore verwenden. (Ich vertraue darauf, dass der Grund offensichtlich ist. Wenn die einzelne CPU damit beschäftigt ist, darauf zu warten, dass etwas anderes die Spin-Sperre freigibt, sie aber auf der einzigen CPU ausgeführt wird, wird die Sperre wahrscheinlich nicht freigegeben, bis der aktuelle Prozess oder Thread vorzeitig beendet wird das O/S, was eine Weile dauern kann und nichts Nützliches passiert, bis die Präemption auftritt.)
Ganz einfach, ein Semaphor ist ein "nachgebendes" Synchronisationsobjekt, ein Spinlock ist ein "busywait". (Semaphore haben noch etwas mehr zu bieten, da sie mehrere Threads synchronisieren, anders als ein Mutex oder Guard oder Monitor oder ein kritischer Abschnitt, der einen Codebereich vor einem einzelnen Thread schützt)
Sie würden unter Umständen ein Semaphor verwenden, aber ein Spinlock verwenden, wenn Sie für eine sehr kurze Zeit sperren - das Sperren ist mit Kosten verbunden, insbesondere wenn Sie viel sperren. In solchen Fällen kann es effizienter sein, eine Weile mit Spinlock zu warten, bis die geschützte Ressource entsperrt wird. Offensichtlich gibt es einen Leistungseinbruch, wenn Sie zu lange drehen.
Wenn Sie länger als ein Thread-Quantum drehen, sollten Sie normalerweise eine Semaphore verwenden.