2011-01-06 5 views
24

Das ist also das Fleisch der Frage: Kann Foo.Bar jemals null zurückgeben? Um dies zu verdeutlichen, kann '_bar' auf null gesetzt werden, nachdem es als nicht-null ausgewertet wurde und bevor sein Wert zurückgegeben wird?Ist der Null-Coalesce-Operator Thread sicher?

public class Foo 
    { 
     Object _bar; 
     public Object Bar 
     { 
      get { return _bar ?? new Object(); } 
      set { _bar = value; } 
     } 
    } 

ich die folgende Methode get wissen Verwendung ist nicht sicher und kann einen Nullwert zurück:

  get { return _bar != null ? _bar : new Object(); } 

UPDATE:

Ein anderer Weg, auf dem gleichen Problem zu suchen, diese Beispiel könnte klarer sein:

 public static T GetValue<T>(ref T value) where T : class, new() 
     { 
      return value ?? new T(); 
     } 

Und a Gewinn fragen kann GetValue (...) jemals null zurückgeben? Abhängig von Ihrer Definition kann dies thread-safe oder nicht sein ... Ich denke, die richtige Problem-Anweisung ist gefragt, ob es eine atomare Operation auf Wert ist ... David Yaw hat die Frage am besten definiert, ist die obige Funktion das Äquivalent auf die folgenden:

 public static T GetValue<T>(ref T value) where T : class, new() 
     { 
      T result = value; 
      if (result != null) 
       return result; 
      else 
       return new T(); 
     } 
+0

Sie konnten die generische Faule Klasse verwenden, anstatt sicher zu initialisieren. http://msdn.microsoft.com/en-us/library/dd642331.aspx – TrueWill

+0

@TrueWill: Es ist schwierig, Lazy zu verwenden, wenn Sie in der Lage sein müssen, einen Setter auf der Eigenschaft zu haben ... –

+0

Gefunden: http://haacked.com/archive/2006/08/08/IsTheNullCoalescingOperatorThreadSafe.aspx –

Antwort

22

Nein, das ist nicht threadsicher.

Die IL für die oben kompiliert:

.method public hidebysig specialname instance object get_Bar() cil managed 
{ 
    .maxstack 2 
    .locals init (
     [0] object CS$1$0000) 
    L_0000: nop 
    L_0001: ldarg.0 
    L_0002: ldfld object ConsoleApplication1.Program/MainClass::_bar 
    L_0007: dup 
    L_0008: brtrue.s L_0010 
    L_000a: pop 
    L_000b: newobj instance void [mscorlib]System.Object::.ctor() 
    L_0010: stloc.0 
    L_0011: br.s L_0013 
    L_0013: ldloc.0 
    L_0014: ret 
} 

Diese effektiv eine Last des _bar Feld der Fall ist, dann überprüft seine Existenz, und springt ot das Ende. Es gibt keine Synchronisation, und da dies mehrere IL-Anweisungen sind, ist es möglich, dass ein sekundärer Thread eine Race-Bedingung verursacht - was dazu führt, dass das zurückgegebene Objekt von dem gesetzten abweicht.

Es ist viel besser, faule Instanziierung über Lazy<T> zu behandeln. Dies bietet ein Thread-sicheres, träges Instanziierungsmuster. Zugegeben, der obige Code ist nicht faul Instanziierung (eher ein neues Objekt jedes Mal bis zu einer bestimmten Zeit, wenn _bar gesetzt ist), aber ich vermute, das ist ein Fehler, und nicht das beabsichtigte Verhalten.

Darüber hinaus macht Lazy<T> die Einstellung schwierig.

Um das obige Verhalten threadsicher zu duplizieren, wäre eine explizite Synchronisation erforderlich.


Zu Ihrer Update:

Der Getter für die Bar Eigenschaft könnte nie null zurück.

Mit Blick auf die IL oben, es _bar (über LDFLD), dann überprüft, ob dieses Objekt nicht 0 ist mit brtrue.s. Wenn das Objekt nicht null ist, springt es, kopiert den Wert _bar vom Ausführungsstapel über stloc.0 in einen lokalen und gibt - _bar mit einem reellen Wert zurück.

Wenn _bar nicht festgelegt war, wird es aus dem Ausführungsstapel ausgeklappt und ein neues Objekt erstellt, das dann gespeichert und zurückgegeben wird.

In beiden Fällen wird verhindert, dass ein Wert null zurückgegeben wird. Allerdings würde ich dies im Allgemeinen nicht als thread-sicher betrachten, da es möglich ist, dass ein Aufruf zum Setzen gleichzeitig mit einem Aufruf zum Abrufen dazu führt, dass verschiedene Objekte zurückgegeben werden, und es ist eine Wettlaufbedingung, welches Objekt Instanz wird zurückgegeben (der gesetzte Wert oder ein neues Objekt).

+0

Multiple IL bedeutet nicht mehrere asm-Anweisungen. Weder andersherum. Ziemlich nicht so. Doch die Antwort ist richtig, wollte nur diese Bemerkung beachten. – Dykam

+0

@Dykam: In diesem Fall sind die fraglichen IL-Anweisungen wirklich auf asm-Anweisungen abgebildet. In der Regel werden einzelne IL-Befehle auf 1 oder mehr asm-Anweisungen abgebildet ... –

+0

Wenn Sie Ihr Update lesen, kann es null zurückgeben, wenn nach dem Test ein _bar = null auftritt (_bar! = Null), habe ich das richtig gelesen? –

2

der Getter wird niemals null zurück.

Dies liegt daran, wenn die Lese auf den Variable (_bar) der Ausdruck und das resultierende Objekt ausgewertet durchgeführt wird (oder null) „frei“ die Variable (_bar) dann. Es ist das Ergebnis dieser ersten Auswertung, die dann an den Koaleszenzoperator "weitergegeben" wird. (Siehe Reed's gute Antwort für die IL.)

Jedoch ist dies nicht Thread-sicher und eine Zuordnung kann leicht aus dem gleichen Grund wie oben verloren gehen.

0

Reflector sagt nein:

List<int> l = null; 
var x = l ?? new List<int>(); 

Compiliert zu:

[STAThread] 
public static void Main(string[] args) 
{ 
    List<int> list = null; 
    if (list == null) 
    { 
     new List<int>(); 
    } 
} 

Welche nicht sicher im Hinblick auf zu sein scheint fädeln Sie erwähnt haben.

4

Ich würde das Wort "thread safe" nicht verwenden, um sich darauf zu beziehen. Stattdessen würde ich die Frage stellen, welche von diesen ist die gleiche wie der Nullkoaleszenzoperator?

get { return _bar != null ? _bar : new Object(); } 

oder

get 
{ 
    Object result = _bar; 
    if(result == null) 
    { 
     result = new Object(); 
    } 
    return result; 
} 

von den anderen Antworten zu lesen, es sieht aus wie es auf den zweiten auf das Äquivalent kompiliert, nicht die erste. Wie Sie bereits angemerkt haben, könnte die erste Null zurückgeben, die zweite jedoch nie.

Ist dieses Gewinde sicher? Technisch, nein. Nach dem Lesen _bar könnte ein anderer Thread _bar ändern, und der Getter würde einen veralteten Wert zurückgeben. Aber von dem, wie du die Frage gestellt hast, denke ich, dass du genau das suchst.

Edit: Hier ist ein Weg, um das ganze Problem zu vermeiden. Da value eine lokale Variable ist, kann sie nicht hinter den Kulissen geändert werden.

public class Foo 
{ 
    Object _bar = new Object(); 
    public Object Bar 
    { 
     get { return _bar; } 
     set { _bar = value ?? new Object(); } 
    } 
} 

Edit 2:

Hier ist die IL ich aus einer Veröffentlichung der Kompilierung zu sehen, die mit meiner Interpretation des IL.

.method public hidebysig specialname instance object get_Bar_NullCoalesce() cil managed 
{ 
    .maxstack 8 
    L_0000: ldarg.0       // Load argument 0 onto the stack (I don't know what argument 0 is, I don't understand this statement.) 
    L_0001: ldfld object CoalesceTest::_bar // Loads the reference to _bar onto the stack. 
    L_0006: dup        // duplicate the value on the stack. 
    L_0007: brtrue.s L_000f     // Jump to L_000f if the value on the stack is non-zero. 
              // I believe this consumes the value on the top of the stack, leaving the original result of ldfld as the only thing on the stack. 
    L_0009: pop        // remove the result of ldfld from the stack. 
    L_000a: newobj instance void [mscorlib]System.Object::.ctor() 
              // create a new object, put a reference to it on the stack. 
    L_000f: ret        // return whatever's on the top of the stack. 
} 

Hier ist, was ich von den anderen Möglichkeiten sehen, es zu tun:

.method public hidebysig specialname instance object get_Bar_IntermediateResultVar() cil managed 
{ 
    .maxstack 1 
    .locals init (
     [0] object result) 
    L_0000: ldarg.0 
    L_0001: ldfld object CoalesceTest::_bar 
    L_0006: stloc.0 
    L_0007: ldloc.0 
    L_0008: brtrue.s L_0010 
    L_000a: newobj instance void [mscorlib]System.Object::.ctor() 
    L_000f: stloc.0 
    L_0010: ldloc.0 
    L_0011: ret 
} 

.method public hidebysig specialname instance object get_Bar_TrinaryOperator() cil managed 
{ 
    .maxstack 8 
    L_0000: ldarg.0 
    L_0001: ldfld object CoalesceTest::_bar 
    L_0006: brtrue.s L_000e 
    L_0008: newobj instance void [mscorlib]System.Object::.ctor() 
    L_000d: ret 
    L_000e: ldarg.0 
    L_000f: ldfld object CoalesceTest::_bar 
    L_0014: ret 
} 

Im IL, ist es offensichtlich, dass es die _bar Feld zweimal mit dem trinary Operator liest, aber nur einmal mit dem Null koaleszieren und das Zwischenergebnis var. Darüber hinaus ist die IL der Null-Koaleszenz-Methode der Zwischenergebnis-Var-Methode sehr ähnlich.

Und hier ist die Quelle, die ich diese zu erzeugen verwendet:

public object Bar_NullCoalesce 
{ 
    get { return this._bar ?? new Object(); } 
} 

public object Bar_IntermediateResultVar 
{ 
    get 
    { 
     object result = this._bar; 
     if (result == null) { result = new Object(); } 
     return result; 
    } 
} 

public object Bar_TrinaryOperator 
{ 
    get { return this._bar != null ? this._bar : new Object(); } 
} 
+0

Richtig bist du, und genau wie ich es normalerweise vermeide, Nullstellen zu setzen; Es ist jedoch eine allgemeinere Frage, um das Verhalten zu verstehen. Immer noch viele widersprüchliche Antworten, und ich kann nicht 100% sicher aus dem Lesen der IL sein; ( –

+0

Siehe bearbeiten, ich postete die IL für die Null Koalesce, der trinary-Operator, und eine Zwischenergebnisvariable verwenden. Die IL für die Null Coalesce ist sehr nah an der IL des Zwischenergebnisses var. Ich habe auch ein paar Kommentare dazu geschrieben, wie ich die IL für die Null-Koaleszenz-Methode lese.Ich bin kein IL-Experte, aber ich denke, dass ich das Wichtige im Griff habe Sachen in dieser Methode. –