2013-02-10 18 views
22

Ein article im MSDN Magazine diskutiert den Begriff der Einführung von Lesen und gibt ein Codebeispiel, das dadurch gebrochen werden kann.Lesen Einführung in C# - Wie kann ich dagegen vorgehen?

Beachten Sie diesen Kommentar "Mai werfen eine NullReferenceException" - ich wusste nie, das war möglich.

Also meine Frage ist: Wie kann ich gegen die Einführung von lesen schützen?

Ich wäre auch sehr dankbar für eine Erklärung, wenn der Compiler beschließt, Lesevorgänge einzuführen, weil der Artikel es nicht enthält.

+5

Ein JIT-Compiler ändern Code Semantik, macht mir Sorgen. Vielleicht war es ein JIT-Fehler vor .NET 4.5. – leppie

+1

Yup, das sieht definitiv wie ein JIT-Bug aus. In der Tat würde ich es als einen Fehler klassifizieren, egal was passiert. Der Artikel ist ein bisschen nutzlos - er erwähnt das Konzept, sagt aber nicht wirklich, wie es vorkommt und wie man sich dagegen wehrt. –

+0

Der Lese-Einführungsteil erwähnt Multi-Threading nicht explizit, aber das Intro sagt: "Der Compiler und die Hardware können die Speicheroperationen eines Programms auf eine Art und Weise transformieren, die Singlethread-Verhalten nicht beeinflusst, aber Multithreading beeinflussen kann Verhalten.". Verwenden Sie eine Sperre wäre die Antwort auf Ihre Frage in diesem Fall. – rene

Antwort

17

Lassen Sie mich versuchen, diese komplizierte Frage zu klären, indem Sie es auflösen.

Was ist "Einführung lesen"?

"Lesen Sie Einführung" ist eine Optimierung, wodurch der Code:

public static Foo foo; // I can be changed on another thread! 
void DoBar() { 
    Foo fooLocal = foo; 
    if (fooLocal != null) fooLocal.Bar(); 
} 

optimiert wird durch die lokale Variable zu eliminieren. Der Compiler kann das , wenn es nur einen Thread gibt dann foo und fooLocal sind das gleiche. Der Compiler darf explizit jede Optimierung vornehmen, die für einen einzelnen Thread unsichtbar wäre, selbst wenn er in einem Multithread-Szenario sichtbar wird. Dem Compiler ist es daher erlaubt, dies wie folgt umzuschreiben:

void DoBar() { 
    if (foo != null) foo.Bar(); 
} 

Und jetzt gibt es eine Race-Bedingung. Wenn foo nach der Überprüfung von Nicht-Null auf Null wechselt, ist es möglich, dass foo ein zweites Mal gelesen wird, und das zweite Mal könnte es null sein, was dann zum Absturz führen würde. Aus der Perspektive der Person, die den Crash-Dump diagnostiziert, wäre dies völlig mysteriös.

Kann das tatsächlich passieren?

Als Artikel, den Sie heraus aufgerufen verknüpft:

Beachten Sie, dass Sie nicht in der Lage sein, die Nullreferenceexception in .NET Framework 4.5 auf x86-x64 mit diesem Code Probe zu reproduzieren. Die Einführung zu lesen ist in .NET Framework 4.5 sehr schwierig zu reproduzieren, tritt jedoch unter bestimmten Umständen dennoch auf.

x86/x64-Chips haben ein "starkes" Speichermodell und die JIT-Compiler sind in diesem Bereich nicht aggressiv; Sie werden diese Optimierung nicht machen.

Wenn Sie Ihren Code auf einem schwachen Speichermodellprozessor wie einem ARM-Chip ausführen, sind alle Wetten deaktiviert.

Wenn Sie "den Compiler" sagen, welchen Compiler meinen Sie?

Ich meine den JIT-Compiler. Der C# -Compiler führt auf diese Weise keine Lesevorgänge ein. (Es ist erlaubt, aber in der Praxis tut es nie.)

Ist es nicht eine schlechte Praxis, Speicher zwischen Threads ohne Speicherbarrieren zu teilen?

Ja. Hier sollte etwas unternommen werden, um eine Speicherbarriere einzuführen, da der Wert foo bereits ein veralteter Cache-Wert im Prozessorcache sein könnte. Meine Präferenz für die Einführung einer Speicherbarriere besteht darin, eine Sperre zu verwenden. Sie können auch das Feld volatile verwenden oder VolatileRead verwenden oder eine der Methoden Interlocked verwenden. All dies führt zu einer Speicherbarriere. (volatile führt nur einen "halben Zaun" FYI ein.)

Nur weil es eine Speicherbarriere gibt, bedeutet das nicht unbedingt, dass Leseeinführungsoptimierungen nicht durchgeführt werden. Der Jitter ist jedoch weit weniger aggressiv bei der Verfolgung von Optimierungen, die sich auf Code auswirken, der eine Speicherbarriere enthält.

Gibt es andere Gefahren dieses Muster?

Sicher! Nehmen wir an, es gibt keine Leseeinführungen. Sie haben immer noch eine Race-Bedingung. Was passiert, wenn ein anderer Thread foo nach der Prüfung auf null setzt, und auch den globalen Status ändert, der Bar verbraucht? Jetzt haben Sie zwei Threads, von denen einer glaubt, dass foo nicht null ist und der globale Status für einen Aufruf von Bar OK ist, und ein anderer Thread, der das Gegenteil glaubt, und Sie Bar ausgeführt werden. Dies ist ein Rezept für eine Katastrophe.

Was ist die beste Vorgehensweise hier?

Erstens, nicht gemeinsamen Speicher über Threads. Diese ganze Idee, dass es zwei Fäden der Kontrolle innerhalb der Hauptlinie Ihres Programms gibt, ist einfach verrückt, um damit zu beginnen. Es hätte niemals eine Sache sein sollen.Verwenden Sie Threads als leichtgewichtige Prozesse. Geben Sie ihnen eine unabhängige Aufgabe, die nicht mit dem Hauptprogramm des Programms interagiert, und verwenden Sie sie nur, um rechenintensive Arbeit durchzuführen.

Zweitens werden Sperren verwenden, um den Speicher auf Threads zu serialisieren. Sperren sind billig, wenn sie nicht angefochten werden, und wenn Sie Streit haben, dann beheben Sie dieses Problem. Low-Lock- und No-Lock-Lösungen sind bekanntermaßen schwer zu korrigieren.

Drittens, wenn Sie Speicher über Threads teilen werden dann jede einzelne Methode, die Sie aufrufen, die beinhaltet, dass der gemeinsame Speicher entweder angesichts der Rennbedingungen robust sein muss, oder die Rennen müssen eliminiert werden. Das ist eine schwere Last, und deshalb sollten Sie nicht dorthin gehen.

Mein Punkt ist: lesen Einführungen sind gruselig, aber ehrlich gesagt sind sie die geringsten Ihrer Sorgen, wenn Sie Code schreiben, der fröhlich Speicher über Threads teilt. Es gibt tausend und andere Dinge, um die wir uns zuerst kümmern müssen.

+0

Also, das kann heute nur auf ARM passieren? – leppie

+0

Vielen Dank für diese großartige Antwort! Ich würde immer noch argumentieren, dass es Situationen gibt, in denen die Einführung des Lesens die einzige Sorge ist. Siehe zum Beispiel die Methode 'WaitWhilePausedAsync' in Stephen Toubs [Implementierung] (http://blogs.msdn.com/b/pfxteam/archive/2013/01/13/cooperativa-pausing-async-methods.aspx) des kooperativen Pausierens . – Gebb

+1

@Gebb: Dein Punkt ist gut gemacht. Dies zeigt, dass es ein guter Grund ist, die Produktion von Threading-Primitiven Leuten wie Stephen Toub und Joe Duffy zu überlassen! Versuche nicht, deine eigenen Primitiven zu schreiben; Erhöhen Sie das Abstraktionsniveau, indem Sie Typen wie "Lazy " oder "Task " verwenden, die von Leuten geschrieben wurden, die das verstehen. –

7

Sie können wirklich nicht gegen die Einführung vorlesen, da es sich um eine Compiler-Optimierung handelt (mit Ausnahme von Debug-Builds ohne Optimierung natürlich). Es ist ziemlich gut dokumentiert, dass der Optimierer die single-threaded-Semantik der Funktion beibehält, die, wie der Artikel feststellt, Probleme in Multithread-Situationen verursachen kann.

Das heißt, ich bin durch sein Beispiel verwirrt. In Jeffrey Richters Buch CLR via C# (in diesem Fall v3) deckt er dieses Muster ab und merkt an, dass es in dem Beispiel-Snippet oben in THEORY nicht funktionieren würde. Aber es war ein empfohlenes Muster von Microsoft in der frühen Existenz von .Net, und deshalb sagten die JIT-Compiler-Leute, mit denen er gesprochen hatte, dass sie sicherstellen müssten, dass diese Art von Ausschnitt niemals bricht. (Es ist immer möglich, dass sie entscheiden, dass es sich lohnt, aus irgendeinem Grund zu brechen - ich kann mir vorstellen, dass Eric Lippert das aufklären könnte).

Schließlich, im Gegensatz zu dem Artikel bietet Jeffrey den „richtigen“ Weg, dies in Situationen mit mehreren Threads zu behandeln (ich habe sein Beispiel mit Ihrem Beispielcode modifiziert):

Object temp = Interlocked.CompareExchange(ref _obj, null, null); 
if(temp != null) 
{ 
    Console.WriteLine(temp.ToString()); 
} 
+0

Nur um das klar zu machen: Der einzige Grund, 'CompareExchange' zu ​​verwenden, ist einen Memory Fence um das Lesen von zu bekommen '_obj', um den Compiler daran zu hindern, seine Shenanigans zu machen, oder? –

+0

Korrigieren. Im Idealfall würden Sie Interlocked.VolatileRead verwenden, aber es hat keine generische Version, daher ist CompareExchange die bessere Lösung. Es ist auch eine bessere Lösung, als _job als flüchtig zu deklarieren, wie Sie flüchtige aus Gründen der Leistung vermeiden sollten, es sei denn, Sie müssen es überall sicher haben, was selten ist. – Gjeltema

1

ich nur den Artikel Mager , aber es scheint, dass, was der Autor sucht, dass Sie das _obj Mitglied als volatile erklären müssen.

+0

Mein erster Gedanke war auch, dass sicher mit privaten flüchtigen Objekt _obj' das konnte nicht scheitern. Aber vielleicht ist meine Intuition falsch. –

+0

Wenn Sie sich darüber Gedanken machen, überprüfen Sie diese Frage: http://StackOverflow.com/questions/394898/double-checked-locking-in-net – erikkallen

+0

@erikkallen: Stephen Toub scheint Ihre Hypothese zu bestätigen. Zumindest habe ich diesen Eindruck von diesem [Gespräch] (http://blogs.msdn.com/b/pfxteam/archive/2013/01/13/cooperativa-pausing-async-methods.aspx) mit ihm bekommen. – Gebb