2013-10-30 10 views
11

Selbst für ein einfaches 2-Faden-Kommunikation Beispiel, ich habe Schwierigkeiten, diese atomaren und memory_fence im C11-Stil zum Ausdruck bringen richtige Speicherordnung zu erhalten:C11 Speicher Zaun Verwendung

gemeinsam genutzter Daten:

volatile int flag, bucket; 
Gewinde

Produzent:

while (true) { 
    int value = producer_work(); 
    while (atomic_load_explicit(&flag, memory_order_acquire)) 
     ; // busy wait 
    bucket = value; 
    atomic_store_explicit(&flag, 1, memory_order_release); 
} 

Verbraucher dre Anzeige:

while (true) { 
    while (!atomic_load_explicit(&flag, memory_order_acquire)) 
     ; // busy wait 
    int data = bucket; 
    atomic_thread_fence(/* memory_order ??? */); 
    atomic_store_explicit(&flag, 0, memory_order_release); 
    consumer_work(data); 
} 

Soweit ich oben Code zu verstehen, Shop-in-Eimer richtig wäre bestellen -> Flag-Speicher -> Flag-Last -> Last-from-Eimer. Ich denke jedoch, dass es zwischen dem Load-from-Bucket und dem Bucket einen neuen Zustand mit neuen Daten gibt. Um eine Order zu erzwingen, die dem Bucket-Read folgt, würde ich einen expliziten atomic_thread_fence() zwischen dem Bucket-Read und dem folgenden Atomic_store benötigen. Leider scheint es kein memory_order Argument zu geben, um irgendetwas auf vorhergehenden Lasten zu erzwingen, nicht einmal die memory_order_seq_cst.

Eine wirklich schmutzige Lösung könnte sein, die bucket im Consumer-Thread mit einem Dummy-Wert neu zuzuordnen: das widerspricht dem Consumer Read-Only-Konzept.

In der älteren C99/GCC-Welt konnte ich die traditionelle __sync_synchronize() verwenden, die meiner Meinung nach stark genug wäre.

Was wäre die bessere C11-Lösung, um diese sogenannte Anti-Abhängigkeit zu synchronisieren?

(Natürlich Ich bin mir bewusst, dass ich besser, solche Low-Level-Codierung vermeiden sollte und Nutzung stehen zur Verfügung geordnete Konstrukte, aber ich würde ... verstehen mag)

+1

Ich bin kein C++ - Programmierer, aber (konzeptionell) bin ich nicht sicher, ob der Aufruf "atomic_thread_fence()" notwendig ist. Die Flag-Aktualisierung hat eine Freigabesemantik, die verhindert, dass irgendwelche vorhergehenden Speicheranweisungen über sie neu angeordnet werden (z. B. der Speicher zu "Daten"). Der Speicher für "Daten" hat eine Abhängigkeit von dem Lesevorgang von "Speicherbereich", so dass der Lesevorgang nicht über die Merkerfreigabe hinaus neu geordnet werden kann. Wenn der volle Zaun notwendig ist, würde ich gerne hören warum. –

+2

Keine Antwort, also nur ein Kommentar: Es scheint so, als ob Sie C11s "atomic_flag" -Datentyp neu erfinden, der genau diese Semantik implementiert, aber letztendlich eine direktere Implementierung in Hardware hat. 'atomic_flag' ist der einzige atomare Datentyp, bei dem die Sperrung garantiert ist. Dies ist gegenüber komplexeren Operationen immer vorzuziehen. Und es würde definitiv keinen zusätzlichen Zaun benötigen, um Konsistenz zu gewährleisten. –

+0

Mike S, deine Antwort scheint mir attraktiv, aber ... Ich dachte, dass Speicherzäune Dinge auf dem Speichersubsystem sicherstellen würden, die ld/st-Ops betreffen. Im obigen Beispiel würden 'Daten' wahrscheinlich zu einer Registervariablen werden, so dass ihre Zuweisung keine Speicheroperation erzeugt. Das würde nur die Last von Bucket für die Speichersynchronisierung belassen? (für die es keine nachfolgende C11-Speicherreihenfolge gibt?) –

Antwort

3

Um eine Reihenfolge nach dem Bucket-Read zu erzwingen, würde ich vermutlich eine explizite atomic_thread_fence() zwischen dem Bucket-Read und dem folgenden Atomic_store benötigen.

Ich glaube nicht, der atomic_thread_fence() Aufruf notwendig: das Flag-Update hat Release Semantik, von einem der vorhergehenden Lade- oder Speicheroperationen zu verhindern über sie neu geordnet werden. Siehe die formale Definition von Herb Sutter:

Eine Zuschreibung Release führt schließlich liest und schreibt vom selben Thread, der es in der Programmreihenfolge vorangestellt werden.

Dies soll das Lesen von bucket vor einem neu geordnet verhindern, nachdem die flag Update auftreten, unabhängig davon, wo der Compiler data speichern wählt.

Das hat mich auf Ihre Kommentare über andere Antwort bringt:

Die volatile stellt sicher, dass es ld/st Operationen erzeugt, die anschließend mit Zäunen können bestellt werden. Daten sind jedoch eine lokale Variable, nicht flüchtig. Der Compiler wird es wahrscheinlich registerieren und eine Speicheroperation vermeiden. Dadurch bleibt die Last aus dem zu bestellenden Bucket mit dem nachfolgenden Flag-Reset.

Es wäre das scheint kein Problem dar, wenn die bucket Lese kann nicht über die Schreibfreigabe flag neu geordnet werden, so sollte volatile nicht notwendig sein (obwohl es wahrscheinlich nicht, es zu haben tut nicht weh, entweder). Es ist auch nicht notwendig, da die meisten Funktionsaufrufe (in diesem Fall atomic_store_explicit(&flag)) als Speicherbarrieren für die Kompilierung dienen. Der Compiler würde das Lesen einer globalen Variablen nach einem nicht inlinierten Funktionsaufruf nicht neu anordnen, da diese Funktion die gleiche Variable ändern könnte.

Ich würde auch mit @MaximYegorushkin zustimmen, dass Sie Ihre busy-waiting mit pause Anweisungen beim Targeting kompatibler Architekturen verbessern können. GCC und ICC scheinen beide _mm_pause(void) intrinsisch zu haben (wahrscheinlich äquivalent zu __asm__ ("pause;")).

+0

Danke Mike für die korrekte Beschreibung. Sie bestätigen also, dass die Beschreibung von 'memory_order_release' auf cppreference.com inkorrekt ist (Beanspruchung nur für Vorgängergeschäfte). In Bezug auf 'volatile' für' bucket': Wenn der Compiler Kenntnis über 'atomic_store_explicit' und Inline-Aufrufe des Aufrufs hätte, könnte ein nichtflüchtiger Bucket so zugeordnet werden, dass einige der beabsichtigten Lasten und der Datenaustausch zwischen den Threads übersprungen werden. –

+0

Es darf nicht _inkorrekt_ an sich sein; Technisch beschreibt Sutter die "Schreib-Freigab" -Semantik, die ich "** ein Schreiben ** mit Freigabesemantik", z. B. "atomic_store_explicit()", bedeuten würde. Dies kann einfach eine stärkere Garantie sein als die, die durch eine einzige Freigabesperre bereitgestellt wird, z. B. "atomic_thread_fence()", in welchem ​​Fall die Dokumentation für "memory_order_release" einfach die _minimalen_ Garantien beschreiben kann. Leider hat es sich als äußerst schwierig erwiesen, autorisierende Antworten auf solche Dinge zu bekommen, aber jede Definition von "Releasesemantik", die ich gesehen habe, stimmt mit der von Sutter überein. –

+0

Die Annahme dieser Antwort bedeutet, dass cppreference.com falsch ist. @Mike, der obige Link zu preshing.com zeigt dies ebenfalls an. Ich habe gerade gefunden http://StackOverflow.com/Questions/16179938, wo die Antwort auch angibt, dass cppreference.com falsch ist: beide sagen, dass eine store.Release sollte auf vorherige Lasten bestellen. Der Entwurf von C++ vom 11. Januar 2012 besagt jedoch: "Informell führt das Ausführen einer Freigabeoperation auf A vorherige Seiteneffekte auf andere Speicherpositionen aus, um für andere Threads sichtbar zu werden". Diese "Nebenwirkungen" schließt jetzt Lesevorgänge aus? Dies schwächt den Fall für die Änderung von 'memory_order_release' auf cppreference.com :-( –

1

Ich stimme mit dem, was @MikeStrobel sagt in sein Kommentar.

Sie brauchen nicht atomic_thread_fence() hier, weil Ihre kritischen Abschnitte mit erwerben beginnen und mit Release-Semantik enden. Daher können Lesevorgänge in Ihren kritischen Abschnitten nicht vor dem Erwerb neu geordnet werden, und nach der Veröffentlichung werden Schreibvorgänge durchgeführt. Und deshalb ist volatile hier auch nicht notwendig.

Außerdem sehe ich keinen Grund, warum (pthread) Spinlock hier nicht verwendet wird. spinlock hat eine ähnliche beschäftigt Spin für Sie, aber es nutzt auch pause instruction:

Die Pause intrinsische in verwendet wird Spin-Warteschleifen mit den Prozessoren Implementierung dynamischer Ausführung (insbesondere Out-of-Order-Ausführung). In der Spin-Wait-Schleife verbessert die innewohnende Pause die Geschwindigkeit, mit der der Code die Freigabe der Sperre erkennt und einen besonders signifikanten Leistungsgewinn liefert. Die Ausführung des nächsten Befehls wird um eine implementierungsspezifische Zeit verzögert. Die PAUSE-Anweisung ändert den Architekturzustand nicht. Für die dynamische Planung reduziert der PAUSE-Befehl den Nachteil des Verlassens der Spin-Schleife.

+0

Wenn 'bucket' nicht als 'volatile' deklariert werden würde, bezweifle ich, dass die Ladevorgänge von' bucket' immer den richtigen Wert zurückliefern: Der volatile scheint notwendig, um zu verhindern, dass der Compiler eine lokale Registerkopie von 'bucket' erstellt. Ich weiß zwar nicht, ob die "volatile" noch für die Flagge benötigt wird, die atomaren Intrinsics könnten dafür Deckung haben. Beachten Sie jedoch, dass die C11-Prototypen dieser Funktionen ein "flüchtiges" Zeigerargument angeben. –

+1

@JosvE Nein, 'volatile' ist unnötig, weil Sie vor und nach dem Zugriff auf Speicherbarrieren haben, das ist alles was zählt. Siehe http://channel9.msdn.com/Shows/Going+Deep/Cpp-and-Beyond-2012-Herb-Sutter-atomic-Weapons-1-of-2 und der zweite Teil für die detaillierte Behandlung Ihrer Frage und ' volatile "auch. –

+1

OK, ging tief durch Sutters Präsentation ... Sie haben Recht damit, dass der Compiler sich um die Bestellung mit den Atomzäunen kümmern muss, keine weiteren "flüchtigen" Deklarationen benötigend. Vielen Dank. –

-1

Direkte Antwort:

, dass Ihr Geschäft ist ein memory_order_release Betrieb bedeutet, dass der Compiler einen Speicher Zaun für Speicherbefehl vor dem Laden der Flagge zu emittieren hat.Dies ist erforderlich, um sicherzustellen, dass andere Prozessoren den endgültigen Status der freigegebenen Daten sehen, bevor sie mit der Interpretation beginnen. Nein, Sie müssen keinen zweiten Zaun hinzufügen.


Lange Antwort:

Wie oben erwähnt, was passiert ist, dass der Compiler Ihre atomic_... Anweisungen in Kombinationen von Zäunen und Speicherzugriffe transformiert; die grundlegende Abstraktion ist nicht die atomare Last, es ist der Erinnerungszaun. So funktionieren die Dinge, auch wenn die neuen C++ - Abstraktionen dazu verleiten, anders zu denken. Und ich persönlich finde Gedächtniszäune viel leichter nachzudenken als die angestrebten Abstraktionen in C++.

Aus Hardware-Sicht, was Sie sicherstellen müssen, ist die relative Bestellung Ihrer Ladungen und speichert, i. e. dass der Schreibvorgang in den Bucket abgeschlossen wird, bevor das Flag in den Producer geschrieben wird, und dass das Flag load einen Wert liest, der älter ist als der Ladevorgang des Buckets im Consumer.

das gesagt ist, was Sie wirklich brauchen, ist dies:

//producer 
while(true) { 
    int value = producer_work(); 
    while (flag) ; // busy wait 
    atomic_thread_fence(memory_order_acquire); //ensure that value is not assigned to bucket before the flag is lowered 
    bucket = value; 
    atomic_thread_fence(memory_order_release); //ensure bucket is written before flag is 
    flag = true; 
} 

//consumer 
while(true) { 
    while(!flag) ; // busy wait 
    atomic_thread_fence(memory_order_acquire); //ensure the value read from bucket is not older than the last value read from flag 
    int data = bucket; 
    atomic_thread_fence(memory_order_release); //ensure data is loaded from bucket before the flag is lowered again 
    flag = false; 
    consumer_work(data); 
} 

Beachten Sie, dass die Etiketten „Erzeuger“ und „Verbraucher“ sind irreführend hier, weil wir zwei Prozesse spielen Ping-Pong haben, jeder Hersteller zu werden und Verbraucher der Reihe nach; es ist nur, dass ein Thread nützliche Werte erzeugt, während die andere „Löcher“ erzeugt in nützliche Werte zu schreiben ...

atomic_thread_fence() alles, was Sie brauchen, und da es unter den an die Assembler-Anweisungen direkt übersetzt atomic_... Abstraktionen, es ist garantiert der schnellste Weg.

+0

Ohne verriegelte (atomare) Lesevorgänge von 'flag' würde ich nicht erwarten, dass der letzte' flag'-Wert gelesen wird, wenn der Erzeuger und der Verbraucher auf getrennten CPUs laufen. Die Updates können bestellt werden, aber die neuen Ergebnisse sind möglicherweise nicht _seen_. Ich erwarte, dass er aus ähnlichen Gründen auch ein verriegeltes Geschäft braucht, wenn er die Flagge aktualisiert. Zumindest sollten die Freigaben-Zäune den "Flag" -Zuweisungen folgen. –

+0

@MikeStrobel Nein, die Releases müssen vor den Flag-Zuweisungen stehen, sie sind die Ladenzäune: Jeder Laden davor muss vor jedem Laden wirksam werden, nachdem er dies getan hat. In Bezug auf Atomizität: Ja, Zuweisungen zu Flag müssen atomar sein, aber zeigen Sie mir die Architektur, in der ein Speicher zu einem richtig ausgerichteten 'int' nicht atomar ist. Wir brauchen dafür keine Sprachunterstützung. – cmaster

+0

Whoops, ja, ignoriere, was ich über das Verschieben der Freigabe Zaun nach dem Laden gesagt habe. In Bezug auf die Atomik war mein Punkt jedoch nicht, dass die Lese- und Schreibvorgänge in "flag" nicht atomar sind, sondern dass sie nicht miteinander verknüpft sind (und vermutlich die atomaren Lese-/Schreibfunktionen in dieser API sind). Wie wird in Ihrer Version sichergestellt, dass andere CPUs nicht versuchen, eine veraltete Version von 'flag' aus ihrem lokalen Cache zu lesen? –