web-dev-qa-db-de.com

Ist meine Spinlock-Implementierung korrekt und optimal?

Ich verwende einen Spin-Lock, um einen sehr kleinen kritischen Bereich zu schützen. Konflikte treten sehr selten auf, so dass eine Spin-Lock-Methode geeigneter ist als ein regulärer Mutex.

Mein aktueller Code lautet wie folgt und setzt x86 und GCC voraus:

volatile int exclusion = 0;

void lock() {
    while (__sync_lock_test_and_set(&exclusion, 1)) {
        // Do nothing. This GCC builtin instruction
        // ensures memory barrier.
    }
}

void unlock() {
    __sync_synchronize(); // Memory barrier.
    exclusion = 0;
}

Ich wundere mich also:

  • Ist dieser Code korrekt? Stellt es den gegenseitigen Ausschluss richtig sicher?
  • Funktioniert es auf allen x86-Betriebssystemen?
  • Funktioniert es auch bei x86_64? Auf allen Betriebssystemen?
  • Ist es optimal?
    • Ich habe Spin-Lock-Implementierungen mit Compare-and-Swap gesehen, bin mir aber nicht sicher, was besser ist.
    • Laut der GCC-Dokumentation zu Atomic Builtins ( http://gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Atomic-Builtins.html ) gibt es auch __sync_lock_release. Ich bin kein Experte für Speicherbarrieren, daher bin ich mir nicht sicher, ob ich dies anstelle von __sync_synchronize verwenden darf.
    • Ich optimiere für den Fall, dass es keine Konflikte gibt.

Überhaupt interessiert mich nicht für Konflikte. Es gibt möglicherweise 1, vielleicht 2 andere Threads, die versuchen, die Spin-Sperre alle paar Tage days zu sperren.

37
Hongli

Also frage ich mich:

* Is it correct?

In dem genannten Zusammenhang würde ich ja sagen.

* Is it optimal?

Das ist eine geladene Frage. Indem Sie das Rad neu erfinden, erfinden Sie auch viele Probleme neu, die durch andere Implementierungen gelöst wurden

  • Ich würde bei einem Fehler eine Abfallschleife erwarten, bei der Sie nicht versuchen, auf das Sperrwort zuzugreifen.

  • Die Verwendung einer vollständigen Barriere im Unlock-Modus erfordert lediglich eine Release-Semantik (aus diesem Grund würden Sie __sync_lock_release verwenden, damit Sie st1.rel auf itanium anstelle von mf oder lwsync auf powerpc erhalten, ...). Wenn Sie sich wirklich nur für x86 oder x86_64 interessieren, sind die hier verwendeten Barrieretypen nicht so wichtig (aber wenn Sie den Sprung zu Intel Itanium für einen HP-IPF-Port machen möchten, möchten Sie dies nicht).

  • sie haben nicht die Anweisung pause (), die Sie normalerweise vor Ihre Abfallschleife stellen würden.

  • wenn es Streit gibt, möchten Sieetwas, Semop oder sogar einen dummen Schlaf in der Verzweiflung. Wenn Sie wirklich die Leistung benötigen, die Sie dadurch erhalten, ist der Futex-Vorschlag wahrscheinlich ein guter. Wenn Sie die Leistung benötigen, die Sie fürmaintenancebenötigen, müssen Sie eine Menge Nachforschungen anstellen.

Beachten Sie, dass in einem Kommentar darauf hingewiesen wurde, dass die Freigabesperre nicht erforderlich war. Dies gilt nicht einmal für x86, da die Release-Barriere auch als Anweisung an den Compiler dient, andere Speicherzugriffe um die "Barriere" nicht zu mischen. Sehr ähnlich dem, was Sie erhalten würden, wenn Sie asm ("" ::: "memory") verwenden würden.

* on compare and swap

Auf x86 wird sync_lock_test_and_set einer xchg-Anweisung zugeordnet, die ein implizites Sperrpräfix hat. Auf jeden Fall der kompakteste generierte Code (insbesondere, wenn Sie ein Byte für das "Sperrwort" anstelle eines int verwenden), aber nicht weniger korrekt als bei Verwendung von LOCK CMPXCHG. Die Verwendung von Compare und Swap kann für anspruchsvollere Algorthims verwendet werden (wie das Setzen eines Zeigers ungleich Null auf Metadaten für den ersten "Kellner" im Schlüsselwort bei einem Fehler).

18
Peeter Joot

Sieht gut aus für mich. Übrigens, hier ist die Implementierung von textbook , die selbst im Streitfall effizienter ist.

void lock(volatile int *exclusion)
{
    while (__sync_lock_test_and_set(exclusion, 1))
        while (*exclusion)
            ;
}
20
sigjuice

In Beantwortung Ihrer Fragen:

  1. Sieht für mich ok aus
  2. Angenommen, das Betriebssystem unterstützt GCC (und GCC hat die Funktionen implementiert); Dies sollte auf allen x86-Betriebssystemen funktionieren. Die GCC-Dokumentation legt nahe, dass eine Warnung ausgegeben wird, wenn sie auf einer bestimmten Plattform nicht unterstützt werden.
  3. Es gibt hier nichts x86-64-spezifisches, also sehe ich nicht, warum nicht. Dies kann erweitert werden, um die von GCC unterstützte any - Architektur abzudecken. Es gibt jedoch möglicherweise bessere Möglichkeiten, dies auf Nicht-x86-Architekturen zu erreichen. 
  4. Sie könnten mit __sync_lock_release() in der unlock()-Situation etwas besser sein; da dies die Sperre verringert und in einem einzigen Vorgang eine Speichersperre hinzufügt. Nehmen Sie jedoch an, dass Ihre Behauptung, dass es selten Konflikte geben wird; es sieht gut aus für mich.
4
DaveR

Wenn Sie eine neuere Version von Linux verwenden, können Sie möglicherweise einen futex - einen "Fast Userspace-Mutex" verwenden:

Eine ordnungsgemäß programmierte Sperre auf Futex-Basis verwendet keine Systemaufrufe, es sei denn, die Sperre wird angegriffen

In dem unbestrittenen Fall, für den Sie mit Ihrem Spinlock optimieren wollen, verhält sich das Futex wie ein Spinlock, ohne dass ein Kernel-Syscall erforderlich ist. Wenn die Sperre umstritten ist, erfolgt das Warten im Kernel ohne "busy-waiting".

3

Ich frage mich, ob die folgende CAS-Implementierung auf x86_64 die richtige ist. Es ist auf meinem i7 X920 Laptop (Fedora 13 x86_64, gcc 4.4.5) fast doppelt so schnell.

inline void lock(volatile int *locked) {
    while (__sync_val_compare_and_swap(locked, 0, 1));
    asm volatile("lfence" ::: "memory");
}
inline void unlock(volatile int *locked) {
    *locked=0;
    asm volatile("sfence" ::: "memory");
}
3
Alex Raybosh

Ich kann nicht zur Korrektheit Stellung nehmen, aber der Titel Ihrer Frage hat eine rote Flagge gesetzt, bevor ich den Fragenkörper überhaupt gelesen habe. Synchronisationsprimitiven sind teuflisch schwer, um Korrektheit zu gewährleisten ... wenn möglich, ist es besser, eine gut entworfene/gepflegte Bibliothek zu verwenden, vielleicht pthreads oder boost :: thread .

2
Jason S

Es gibt einige falsche Annahmen. 

Erstens ist SpinLock nur sinnvoll, wenn die Ressource auf einer anderen CPU gesperrt ist. Wenn Ressourcenressource auf derselben CPU gesperrt ist (was bei Einzelprozessorsystemen immer der Fall ist), müssen Sie den Scheduler entspannen, um Ressourcen freizugeben. Ihr aktueller Code funktioniert auf einem Einprozessor-System, da der Scheduler die Aufgaben automatisch wechselt, jedoch Ressourcenverschwendung bedeutet.

Auf einem System mit mehreren Prozessoren kann dasselbe passieren, aber die Task kann von einer CPU zu einer anderen migrieren. Kurz gesagt, die Verwendung der Spin-Lock-Funktion ist korrekt, wenn Sie sicherstellen, dass Ihre Aufgaben auf einer anderen CPU ausgeführt werden.

Zweitens: Sperren eines Mutex IS schnell (so schnell wie Spinlock), wenn is nicht gesperrt ist. Das Sperren und Entriegeln von Mutexen ist nur langsam (sehr langsam), wenn der Mutex bereits gesperrt ist.

In Ihrem Fall schlage ich vor, Mutexe zu verwenden.

0
Jezz

Eine Verbesserung ist die Verwendung von TATAS (test-and-test-and-set). Die Verwendung von CAS-Vorgängen ist für den Prozessor als ziemlich teuer anzusehen, daher sollten Sie sie möglichst vermeiden, wenn Sie dies tun. Eine andere Sache, stellen Sie sicher, dass Sie keine Prioritätsumkehrung erleiden (was ist, wenn ein Thread mit einer hohen Priorität versucht, die Sperre zu erhalten Während ein Thread mit niedriger Priorität versucht, die Sperre aufzuheben? In Windows zum Beispiel wird dieses Problem letztendlich vom Scheduler mit einer Prioritätserhöhung gelöst, Sie können jedoch die Zeitscheibe Ihres Threads explizit aufgeben, falls Sie den Zugriff nicht erhalten haben sperren Sie in den letzten 20 Versuchen (zum Beispiel ..)

0
unknown

Ihr Entsperrvorgang benötigt keine Speicherbarriere. Die Zuweisung zum Ausschluss ist atomar, solange sie auf dem x86 ausgerichtet ist.

0
Ira Baxter

Im konkreten Fall von x86 (32/64) glaube ich nicht, dass Sie im Entriegelungscode überhaupt einen Speicherzaun benötigen. x86 führt keine Neuordnung durch, es sei denn, Stores werden zuerst in einen Store-Puffer gestellt und können daher für andere Threads verzögert werden. Ein Thread, der einen Speicher ausführt und dann aus derselben Variablen liest, liest aus seinem Speicherpuffer, wenn er noch nicht in den Speicher geschrieben wurde. Sie brauchen also nur eine asm -Anweisung, um eine Neuordnung des Compilers zu verhindern. Es besteht die Gefahr, dass ein Thread die Sperre aus der Perspektive anderer Threads etwas länger hält als erforderlich. Wenn Sie sich jedoch nicht um Konflikte kümmern, sollte dies keine Rolle spielen. Tatsächlich ist pthread_spin_unlock wie das auf meinem System (Linux x86_64) implementiert. 

Mein System implementiert auch pthread_spin_lock mit lock decl lockvar; jne spinloop; anstelle von xchg (was __sync_lock_test_and_set verwendet), aber ich weiß nicht, ob es tatsächlich einen Leistungsunterschied gibt. 

0
JanKanis