2010-04-27 6 views
11

Ich möchte die Synchronisation minimieren und schreiben lock-freie Code wenn möglich in einem Projekt von mir. Wenn es absolut notwendig ist, würde ich gerne leichte Spinlocks ersetzen, die aus atomaren Operationen für pthread und win32 Mutex Locks bestehen. Mein Verständnis ist, dass dies Systemaufrufe unterhalb sind und einen Kontextwechsel verursachen könnten (was für sehr schnelle kritische Abschnitte unnötig sein kann, in denen es einfach wäre, sich ein paar Mal zu drehen).Leichte Spinlocks, die aus atomaren Operationen des GCC gebaut wurden?

Die atomaren Operationen Ich beziehe mich hier gut dokumentiert: http://gcc.gnu.org/onlinedocs/gcc-4.4.1/gcc/Atomic-Builtins.html

Hier ist ein Beispiel zu veranschaulichen, was ich rede. Stellen Sie sich einen RB-Baum mit mehreren Lesern und Schreibern vor. RBTree :: exists() ist schreibgeschützt und Thread-sicher, RBTree :: insert() würde exklusiven Zugriff durch einen einzigen Schreiber (und keine Leser) erfordern, um sicher zu sein. Einige Code:

class IntSetTest 
{ 
private: 
    unsigned short lock; 
    RBTree<int>* myset; 

public: 
    // ... 

    void add_number(int n) 
    { 
     // Aquire once locked==false (atomic) 
     while (__sync_bool_compare_and_swap(&lock, 0, 0xffff) == false); 

     // Perform a thread-unsafe operation on the set 
     myset->insert(n); 

     // Unlock (atomic) 
     __sync_bool_compare_and_swap(&lock, 0xffff, 0); 
    } 

    bool check_number(int n) 
    { 
     // Increment once the lock is below 0xffff 
     u16 savedlock = lock; 
     while (savedlock == 0xffff || __sync_bool_compare_and_swap(&lock, savedlock, savedlock+1) == false) 
      savedlock = lock; 

     // Perform read-only operation  
     bool exists = tree->exists(n); 

     // Decrement 
     savedlock = lock; 
     while (__sync_bool_compare_and_swap(&lock, savedlock, savedlock-1) == false) 
      savedlock = lock; 

     return exists; 
    } 
}; 

(lässt annehmen, dass es nicht exception-sicher zu sein braucht)

Ist dieser Code in der Tat Thread-sicher? Gibt es irgendwelche Vor-/Nachteile für diese Idee? Irgendein Rat? Ist die Verwendung von Spinlocks so eine schlechte Idee, wenn die Threads nicht wirklich gleichzeitig sind?

Vielen Dank im Voraus. ;)

+3

Die Antwort, die ich in einer ähnlichen Frage gab, http://stackoverflow.com/questions/1919135/critical-sections-that-spin-on-posix/1923218#1923218, wird wahrscheinlich hier relevant sein. –

+0

Ihre Antwort war definitiv relevant für die Verwendung von Spinlocks im Allgemeinen. Sie scheinen eine gute Idee für SMP-Maschinen in ihrem typischen Fall zu sein. Würde die Worst-Case-Situation (ein Schreiber, der während des kritischen Abschnitts nicht mehr läuft) mit dem wahrscheinlicheren Fall, dass zwei gleichzeitige Threads gleichzeitig versuchen einzufügen, ausgeglichen sein? Wie sieht es in einer hybriden Threading-Umgebung aus, in der Benutzer-Threads einer Anzahl von Kernel-Threads zugeordnet sind, die der Anzahl der logischen Prozessoren auf der Maschine entspricht? Die Worst-Case-Situation wäre dann noch weniger wahrscheinlich; Nein? – Thomas

+0

Ich bin mir nicht sicher, inwieweit die Anzahl der Kernel-Threads die Wahrscheinlichkeit beeinflusst, dass Leistungsprobleme auftreten. Es ist möglich, dass der Writer-Thread gerade seinen Zeitschlitz zwischen dem Eingang und dem Ausgang der Sperre verbraucht hat, was zu dem Problemfall führen würde, egal wie viele Kernel-Threads es gibt. An dieser Stelle bemerke ich, dass die RB-Baum-Einfügeoperation O (log (n)) ist. Je größer also der Baum, desto wahrscheinlicher ist dieses Problem. Außerdem verursacht ein größerer Baum wahrscheinlich Seitenfehler während der Aktualisierung, wodurch auch der Problemfall wahrscheinlicher wird. Ich würde Spinlocks hier vermeiden. –

Antwort

4

Sie benötigen eine volatile Qualifier auf lock, und ich würde auch eine sig_atomic_t machen. Ohne die volatile Qualifier, dieser Code:

u16 savedlock = lock; 
    while (savedlock == 0xffff || __sync_bool_compare_and_swap(&lock, savedlock, savedlock+1) == false) 
     savedlock = lock; 

nicht lock wieder lesen, wenn savedlock in dem Körper der while-Schleife zu aktualisieren. Betrachten Sie den Fall, dass lock 0xffff ist. Dann wird vor der Überprüfung der Schleifenbedingung 0xffff sein, so dass der while Zustand vor dem Aufruf __sync_bool_compare_and_swap kurzgeschlossen wird. Da __sync_bool_compare_and_swap nicht aufgerufen wurde, tritt der Compiler keine Speicherbarriere auf, so dass vernünftigerweise davon ausgegangen werden kann, dass sich der Wert lock unter Ihnen nicht geändert hat, und vermeiden Sie es erneut in .

Re: sig_atomic_t, gibt es eine anständige Diskussion here. Die gleichen Überlegungen, die für Signalhandler gelten, würden auch für Threads gelten.

Mit diesen Änderungen würde ich meinen, dass Ihr Code threadsicher wäre. Ich würde jedoch immer noch Mutexe empfehlen, da Sie wirklich nicht wissen, wie lange Ihr RB-Tree-Einsatz im allgemeinen Fall dauern wird (nach meinen vorherigen Kommentaren unter der Frage).

+0

Das ist interessant. Ich habe viele Artikel gelesen, die erklären, warum volatile der beste Freund eines Multi-Thread-Programms ist, und viele erklären, warum Volatilität nichts damit zu tun hat und alles volatil macht, wird das Programm einfach verlangsamen. In meiner Anwendung kann auf mehr als die Hälfte der Daten von jedem Thread und zu jeder Zeit zugegriffen werden. Sollten sie wirklich alle flüchtig sein? Oder ist dies die Ausnahme, da der Compiler in einer engen Schleife optimiert ist, um die Sperre nur einmal zu überprüfen? – Thomas

+0

, d. H. Bild Eine Funktion (die nicht inline ist) wird aufgerufen, prüft eine Variable, kehrt dann zurück und wird schnell wieder aufgerufen. In diesem Fall wäre volatile nicht notwendig, weil der Compiler nicht in der Lage wäre, Code über mehrere Aufrufe hinweg zu optimieren? Aber in der Schleife oben könnte es erkennen, dass Schloss könnte nie ändern und optimieren Sie es? So flüchtig hat nichts mit Caching zu tun, es teilt einfach dem Compiler mit, den Zugriff auf den Speicher nicht zu optimieren. Ich denke, das ergab nur Sinn für mich. Bitte bestätigen oder klären! :) – Thomas

+1

Ich verbrachte einige Zeit damit zu schauen, wie volatil funktioniert ...Kurz gesagt, was es tut, ist die Optimierung der Speicherzugriffe zu verhindern und auch die Neuordnung von Speicheroperationen mit flüchtigen Variablen zu verhindern. (Speicheroperationen mit nichtflüchtigen, qualifizierten Variablen können um diejenigen, die volatile enthalten, neu geordnet werden. Ferner, auch wenn die Schreibvorgänge der Reihe nach auftreten, kann eine andere CPU die neuen Werte in einer anderen Reihenfolge bemerken.) Dies sollte für multi ausreichen Synchronisation in diesem Fall, weil Sie auch die '__sync'-Routinen haben, die eine Speicherbarriere bieten. –

1

Es ist vielleicht erwähnenswert, dass wenn Sie die Win32-Mutexe verwenden, dass ab Vista ein Thread-Pool für Sie bereitgestellt wird. Abhängig davon, für was Sie den RB-Baum verwenden, können Sie damit ersetzen.

Auch, was Sie beachten sollten, ist, dass atomare Operationen nicht besonders schnell sind. Microsoft sagte, dass sie ein paar hundert Zyklen waren.

Anstatt zu versuchen, die Funktion auf diese Weise zu "schützen", wäre es wahrscheinlich effizienter, die Threads einfach zu synchronisieren, entweder zu einem SIMD/Thread-Pool-Ansatz zu wechseln oder einfach einen Mutex zu verwenden.

Aber natürlich, ohne Ihren Code zu sehen, kann ich wirklich keine Kommentare mehr machen. Das Problem mit Multithreading ist, dass Sie das gesamte Modell von jemandem sehen müssen, um es zu verstehen.

+0

Nun ein weiterer wichtiger Punkt ist der ganze "leichte" Aspekt von diesem. Dies ist nur ein Beispiel, aber in meinem tatsächlichen Code könnte es in einigen Fällen Millionen dieser Objekte geben und ich denke nicht, dass es praktisch wäre, Millionen von Pthread- oder Win32-Mutexen zu erstellen. Ein vorzeichenloser 16-Bit-Int würde eigentlich keinen zusätzlichen Overhead verursachen (wegen der Ausrichtung). – Thomas

+0

Tatsächlich ist der Thread-Pool (http://msdn.microsoft.com/en-us/library/ms684957(VS.85).aspx) seit Windows 2000 verfügbar. –

+0

Es ist nicht praktisch, Millionen von Interlock-Operationen zu verwenden. Ich denke immer noch, dass Sie Ihr Threading-Modell neu entwerfen müssen. Sie scheinen eine Klasse zu entwerfen, die leistungsstark ist und unwissend torkelt. @Billy ONeal - Sie haben Recht. Ich hatte diese Funktion vorher nicht bemerkt. – Puppy

Verwandte Themen