2011-01-06 33 views
24

Así que este es el meollo de la cuestión: ¿Puede Foo.Bar volver alguna vez a ser nulo? Para aclarar, ¿puede '_bar' establecerse en nulo después de que se evalúa como no nulo y antes de que se devuelva su valor?¿Es seguro el hilo del operador de fusión nulo?

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

que conozco utilizando el siguiente método get no es seguro, y puede devolver un valor nulo:

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

ACTUALIZACIÓN:

Otra manera de mirar el mismo problema, este el ejemplo podría ser más claro:

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

Y a ganar pregunta puede GetValue (...) alguna vez devolver nulo? Dependiendo de su definición, esto puede o no ser seguro para subprocesos ... Supongo que el enunciado correcto del problema es preguntar si se trata de una operación atómica sobre el valor ... David Yaw ha definido mejor la pregunta diciendo que la función anterior es equivalente a lo siguiente:

 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

Se puede utilizar la clase genérica Lazy lugar para inicializar de forma segura. http://msdn.microsoft.com/en-us/library/dd642331.aspx – TrueWill

+0

@TrueWill: es difícil usar Lazy cuando necesita tener un setter en la propiedad, también ... –

+0

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

Respuesta

22

No, esto no es seguro para subprocesos.

La IL aplica a la anterior compila a:

.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 
} 

Esto hace efectivamente una carga del campo _bar, a continuación, comprueba su existencia, y salta OT al final. No hay sincronización en su lugar, y dado que se trata de múltiples instrucciones de IL, es posible que un hilo secundario cause una condición de carrera, lo que hace que el objeto devuelto difiera del que se configuró.

Es mucho mejor manejar la creación de instancias perezosas a través de Lazy<T>. Eso proporciona un patrón de ejecución de instancias perezoso y seguro para subprocesos. De acuerdo, el código anterior no está haciendo instancias perezosas (en lugar de devolver un nuevo objeto cada vez que está habilitado), pero sospecho que es un error y no el comportamiento previsto.

Además, Lazy<T> dificulta la configuración.

Para duplicar el comportamiento anterior de una manera segura para hilos requeriría una sincronización explícita.


En cuanto a su actualización

El captador de la propiedad Bar nunca podría devolver null.

Mirando el IL anterior, es _bar (a través de ldfld), luego verifica si ese objeto no es nulo usando brtrue.s. Si el objeto no es nulo, salta, copia el valor de _bar de la pila de ejecución en un local a través de stloc.0, y lo devuelve - devolviendo _bar con un valor real.

Si _bar se ha desactivado, saltará de la pila de ejecución y creará un nuevo objeto que luego se almacenará y se devolverá.

Cualquier caso impide que se devuelva un valor de null. Sin embargo, una vez más, no consideraría este thread-safe en general, ya que es posible que una llamada al set ocurra al mismo tiempo que una llamada para obtener puede devolver diferentes objetos, y es una condición de carrera como qué objeto la instancia se devuelve (el valor establecido o un objeto nuevo).

+0

IL múltiple no significa múltiples instrucciones asm. Ni al revés. Bastante no tan. Sin embargo, la respuesta es correcta, solo quería tomar nota de esta observación. – Dykam

+0

@Dykam: en este caso, las instrucciones de IL en cuestión realmente están mapeando a las instrucciones de asm. Normalmente, las instrucciones individuales de IL se asignarán a 1 o más instrucciones de asm ... –

+0

Entonces, al leer su actualización, puede devolver nulo si un conjunto _bar = null ocurre después de la prueba de (_bar! = Null), ¿lo leí correctamente? –

2

el captador no volverá nula.

Esto es porque cuando la lectura se realiza en el variable de (_bar) se evalúa la expresión y el objeto resultante (o nula) es entonces "libre" de la variable de (_bar). Es el resultado de esta primera evaluación que luego se "pasa" al operador de fusión. (Consulte la buena respuesta de Reed para el IL.)

Sin embargo, esto no es seguro para subprocesos y una tarea se puede perder fácilmente por la misma razón que la anterior.

0

Reflector dice que no:

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

compila en:

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

que no parece ser seguro para subprocesos en el respeto que se ha mencionado.

4

No utilizaría la palabra 'hilo seguro' para referirse a esto. En cambio, me gustaría hacer la pregunta, ¿cuál de estos es el mismo que el operador de fusión nulo?

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

o

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

De la lectura de las otras respuestas, parece que se compila en el equivalente a la segunda, no el primero. Como notó, el primero podría devolver nulo, pero el segundo nunca lo hará.

¿Este thread es seguro? Técnicamente, no. Después de leer _bar, un hilo diferente podría modificar _bar, y el captador devolvería un valor que está desactualizado. Pero por la forma en que hizo la pregunta, creo que esto es lo que está buscando.

Editar: Aquí hay una manera de hacer esto que evita todo el problema. Como value es una variable local, no se puede cambiar entre bastidores.

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

Edición 2:

Aquí está la IL veo de una compilación de lanzamiento, con mi interpretación de la 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. 
} 

Esto es lo que veo de las otras maneras de hacerlo:

.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 
} 

En el IL, es obvio que se está leyendo el campo _bar dos veces con el operador ternario, pero sólo una vez con el nula se unen y el resultado intermedio var. Además, el IL del método de fusión nulo está muy cerca del método de var de resultado intermedio.

Y aquí está la fuente que utiliza para generar los siguientes:

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

Correcto, y precisamente cómo normalmente evito establecer valores nulos; Sin embargo, es una pregunta más general para entender su comportamiento. Aún hay muchas respuestas contradictorias, y no puedo estar 100% seguro al leer el IL; ( –

+0

Ver edición, publiqué el IL para el coalesce nulo, el operador trinario y el uso de una variable de resultado intermedia. coalesce está muy cerca del IL del resultado intermedio var. También puse algunos comentarios sobre cómo leo el IL para el método de coalescencia nula. No soy un experto en IL, pero creo que tengo una idea de lo importante cosas en ese método. –

Cuestiones relacionadas