31

C# 4 in Kürze (stark btw empfohlen) Der folgende Code verwendet das Konzept des Memory (vorausgesetzt, dass A und B auf unterschiedlichen Fäden wurden ausgeführt) zu demonstrieren:Warum brauche ich eine Speicherbarriere?

class Foo{ 
    int _answer; 
    bool complete; 
    void A(){ 
    _answer = 123; 
    Thread.MemoryBarrier(); // Barrier 1 
    _complete = true; 
    Thread.MemoryBarrier(); // Barrier 2 
    } 
    void B(){ 
    Thread.MemoryBarrier(); // Barrier 3; 
    if(_complete){ 
     Thread.MemoryBarrier(); // Barrier 4; 
     Console.WriteLine(_answer); 
    } 
    } 
} 

sie erwähnen, dass Barriers 1 & 4 dies verhindern Beispiel vom Schreiben 0 und Barrieren 2 & 3 bietet eine Frische Garantie: sie sorgen dafür, dass, wenn B lief nach A, Lesen _COMPLETE zu wahren bewerten würde.

Ich bekomme es nicht wirklich. Ich glaube, ich verstehe, warum Barrieren 1 & 4 sind notwendig: Wir wollen nicht auf die Schreiben auf _answer optimiert und nach dem Schreiben auf _COMPLETE (Barrier 1), und wir müssen dafür sorgen, platziert werden, dass _answer ist nicht zwischengespeichert (Barriere 4). Ich glaube auch, dass ich verstehe, warum Barrier 3 notwendig ist: Wenn A bis _complete = true ausgeführt wurde, müsste B noch _complete aktualisieren, um den richtigen Wert zu lesen.

Ich verstehe nicht, obwohl wir Barrier 2 brauchen! Ein Teil von mir sagt, dass es vielleicht daran liegt, dass Thread 2 (läuft B) bereits lief (aber nicht inklusive) wenn (_complete) und so müssen wir sicherstellen, dass _complete aktualisiert wird.

Allerdings sehe ich nicht, wie das hilft. Ist es nicht immer möglich, dass _complete in A auf True gesetzt wird, aber die B-Methode eine gecachete (falsche) Version von _complete? Dh wenn Thread 2 Verfahren B lief bis nach dem ersten Memory und dann Thread 1 Verfahren A bis _COMPLETE = true lief, aber nicht weiter, und dann wieder aufgenommen Gewinde 1 und getestet if (_COMPLETE) - könnte, dass, wenn nicht false?

+0

Warum sollte jemand dies über "volatile" verwenden? – ChaosPandion

+6

@Chaos: CLR über C# Buch (Richter) hat eine großartige Erklärung - IIRC ist, dass 'flüchtig' bedeutet, dass alle Zugriffe auf die Var als flüchtig behandelt werden und volle Speicherbarrieren in beiden Richtungen erzwingen. Das ist oft viel perfekter als nötig, wenn Sie stattdessen nur eine Lese- oder eine Schreibbarriere benötigen und nur bestimmte Zugriffe. –

+1

@Chaos: nicht wirklich der Punkt, aber ein Grund ist, dass volatile hat seine eigenen Macken in Bezug auf Compiler-Optimierungen, die zu Deadlock führen können, siehe http://www.bluebytesoftware.com/blog/2009/02/24/TheMagicalDuelingDeadlockingSpinLocks .aspx – hackerhasid

Antwort

24

Barrier # 2 garantiert, dass der Schreibvorgang auf _complete sofort ausgeführt wird. Andernfalls könnte es in einem eingereihten Zustand bleiben, was bedeutet, dass das Lesen von _complete in B die durch A verursachte Änderung nicht sehen würde, obwohl B effektiv eine flüchtige Leseoperation verwendet.

Natürlich wird dieses Beispiel dem Problem nicht ganz gerecht, weil A nach dem Schreiben auf _complete nichts mehr tut, was bedeutet, dass der Schreibvorgang sowieso sofort durchgeführt wird, da der Thread vorzeitig beendet wird.

Die Antwort auf Ihre Frage, ob die if noch zu false auswerten könnte, ist ja genau aus den Gründen, die Sie angegeben haben. Beachten Sie jedoch, was der Autor zu diesem Punkt sagt.

Barriers 1 und 4 verhindert, dass dieses Beispiel von „0“ zu schreiben. Barrieren 2 und 3 bieten eine Frische-Garantie: sie stellen sicher, dass , wenn B nach A lief, _complete würde als wahr ausgewertet werden.

Die Betonung auf "wenn B nach A lief" gehört mir. Es könnte sicherlich der Fall sein, dass sich die beiden Threads verschachteln. Aber der Autor ignorierte dieses Szenario vermutlich, um seinen Standpunkt dahingehend zu verdeutlichen, wie es einfacher funktioniert.

Übrigens hatte ich es schwer, ein Beispiel auf meiner Maschine zu finden, wo die Barrieren # 1 und # 2 das Verhalten des Programms verändert hätten. Dies liegt daran, dass das Speichermodell für Schreibvorgänge in meiner Umgebung stark war. Vielleicht hätte ich, wenn ich einen Multiprozessor hatte, Mono benutzt oder ein anderes Setup gehabt hätte, es hätte demonstrieren können. Es war natürlich leicht nachzuweisen, dass die Beseitigung der Barrieren Nr. 3 und Nr. 4 Auswirkungen hatte.

+0

Danke, das war hilfreich. Ich denke ich war nicht so ahnungslos wie ich dachte. – hackerhasid

+0

Ich verstehe nicht, dass Barriere 2 * und * 3 für den Fall benötigt werden, dass B nach A läuft. Beide sind volle Zäune, also würde jeder von ihnen alleine tun, nicht wahr? –

+5

@ohadsc: Speicherbarrieren beeinflussen nur das Verhalten eines einzelnen Threads. Beachten Sie, dass A und B möglicherweise auf verschiedenen CPUs ausgeführt werden. Wenn Sie die Barriere 2 entfernt haben, wird der Schreibvorgang möglicherweise nicht ausgeführt. Wenn Sie die Barriere 3 entfernt haben, wird der Lesevorgang möglicherweise nicht aktualisiert. Die Barrieren in A haben keinen Einfluss auf die Ausführung von B und umgekehrt. –

0

Das Beispiel ist aus zwei Gründen unklar:

  1. Es ist zu einfach vollständig zeigen, was mit den Zäunen passiert.
  2. Albahari enthält Anforderungen für Nicht-x86-Architekturen. Siehe MSDN: "MemoryBarrier wird nur auf Multiprozessorsystemen mit schwacher Speicherordnung benötigt (z. B. ein System, das mehrere Intel Itanium-Prozessoren verwendet [die Microsoft nicht mehr unterstützt])."

Wenn Sie die folgende betrachten, wird es deutlicher:

  1. eine Speicherbarriere (Vollschranken hier - .Net nicht eine halbe Barriere bietet) lesen verhindert/Schreibbefehle von dem Zaun springen (aufgrund verschiedener Optimierungen). Dies garantiert uns den Code, nachdem der Zaun nach dem Code vor dem Zaun ausgeführt wird.
  2. „Diese Serialisierungsoperation garantiert, dass jeder Lade- und Speicherbefehl, der der Befehl ist MFENCE global sichtbar vor jeder Lade- oder Speicherbefehl in der Programmreihenfolge vorangeht, die der MFENCE Befehl folgt global sichtbar ist.“ Siehe here.
  3. x86 CPUs haben ein starkes Speichermodell und Garantieschreiben erscheinen konsistent für alle Threads/Kerne (daher sind Barrieren # 2 & # 3 nicht erforderlich auf x86). Aber wir können nicht garantieren, dass Lese- und Schreibvorgänge in codierter Reihenfolge bleiben, daher die Notwendigkeit der Barrieren # 1 und # 4.
  4. Speicherbarrieren sind ineffizient und müssen nicht verwendet werden (siehe den gleichen MSDN-Artikel). Ich benutze persönlich Interlocked und volatile (stellen Sie sicher, dass Sie wissen, wie man es richtig verwendet !!), die effizient arbeiten und leicht zu verstehen sind.

Ps. This article erklärt die inneren Funktionen von x86 schön.