2010-05-14 7 views
37

Supongo que este es un ejemplo de código interesante.Instrucción de cerradura vs Monitor.Entra el método

Tenemos una clase - llamémoslo prueba - con un métodoFinalizar. En el método Principal hay dos bloques de código donde estoy usando una instrucción de bloqueo y una llamada a Monitor.Enter(). Además, tengo dos instancias de la clase Test aquí. El experimento es bastante simple: anule la variable Pruebe variable dentro del bloque de bloqueo y luego intente recopilarlo manualmente con la llamada al método GC.Collect. Entonces, para ver el Finalize llame Estoy llamando al GC.WaitForPendingFinalizers método. Todo es muy simple, como puedes ver.

Por la definición de la cerradura declaración, es abierto por el compilador para la tratar {...} finalmente {..} bloque, con un Monitor.Enter llamada interior de la Pruebe bloque y Monitor. Luego sale en el bloque finally. Intenté implementar manualmente el bloque try-finally.

He esperado el mismo comportamiento en ambos casos: el de utilizar el bloqueo y el de usar Monitor.Introducir. Pero, sorpresa, sorpresa, es diferente, como se puede ver a continuación:

public class Test 
{ 
    private string name; 

    public Test(string name) 
    { 
     this.name = name; 
    } 

    ~Test() 
    { 
     Console.WriteLine(string.Format("Finalizing class name {0}.", name)); 
    } 
} 

class Program 
{ 
    static void Main(string[] args) 
    { 
     var test1 = new Test("Test1"); 
     var test2 = new Test("Tesst2"); 
     lock (test1) 
     { 
      test1 = null; 
      Console.WriteLine("Manual collect 1."); 
      GC.Collect(); 
      GC.WaitForPendingFinalizers(); 
      Console.WriteLine("Manual collect 2."); 
      GC.Collect(); 
     } 

     var lockTaken = false; 
     System.Threading.Monitor.Enter(test2, ref lockTaken); 
     try { 
      test2 = null; 
      Console.WriteLine("Manual collect 3."); 
      GC.Collect(); 
      GC.WaitForPendingFinalizers(); 
      Console.WriteLine("Manual collect 4."); 
      GC.Collect(); 
     } 
     finally { 
      System.Threading.Monitor.Exit(test2); 
     } 
     Console.ReadLine(); 
    } 
} 

La salida de este ejemplo es:

Manual de recoger 1. Manual 2. recoger Manual de recoger 3. La finalización de clases nombre Test2. Recolección manual 4. Y excepción de referencia nula en el último bloque final porque test2 es referencia nula.

Me sorprendió y desensambló mi código en IL. Así pues, aquí es el vertedero de IL método Main:

.entrypoint 
.maxstack 2 
.locals init (
    [0] class ConsoleApplication2.Test test1, 
    [1] class ConsoleApplication2.Test test2, 
    [2] bool lockTaken, 
    [3] bool <>s__LockTaken0, 
    [4] class ConsoleApplication2.Test CS$2$0000, 
    [5] bool CS$4$0001) 
L_0000: nop 
L_0001: ldstr "Test1" 
L_0006: newobj instance void ConsoleApplication2.Test::.ctor(string) 
L_000b: stloc.0 
L_000c: ldstr "Tesst2" 
L_0011: newobj instance void ConsoleApplication2.Test::.ctor(string) 
L_0016: stloc.1 
L_0017: ldc.i4.0 
L_0018: stloc.3 
L_0019: ldloc.0 
L_001a: dup 
L_001b: stloc.s CS$2$0000 
L_001d: ldloca.s <>s__LockTaken0 
L_001f: call void [mscorlib]System.Threading.Monitor::Enter(object, bool&) 
L_0024: nop 
L_0025: nop 
L_0026: ldnull 
L_0027: stloc.0 
L_0028: ldstr "Manual collect." 
L_002d: call void [mscorlib]System.Console::WriteLine(string) 
L_0032: nop 
L_0033: call void [mscorlib]System.GC::Collect() 
L_0038: nop 
L_0039: call void [mscorlib]System.GC::WaitForPendingFinalizers() 
L_003e: nop 
L_003f: ldstr "Manual collect." 
L_0044: call void [mscorlib]System.Console::WriteLine(string) 
L_0049: nop 
L_004a: call void [mscorlib]System.GC::Collect() 
L_004f: nop 
L_0050: nop 
L_0051: leave.s L_0066 
L_0053: ldloc.3 
L_0054: ldc.i4.0 
L_0055: ceq 
L_0057: stloc.s CS$4$0001 
L_0059: ldloc.s CS$4$0001 
L_005b: brtrue.s L_0065 
L_005d: ldloc.s CS$2$0000 
L_005f: call void [mscorlib]System.Threading.Monitor::Exit(object) 
L_0064: nop 
L_0065: endfinally 
L_0066: nop 
L_0067: ldc.i4.0 
L_0068: stloc.2 
L_0069: ldloc.1 
L_006a: ldloca.s lockTaken 
L_006c: call void [mscorlib]System.Threading.Monitor::Enter(object, bool&) 
L_0071: nop 
L_0072: nop 
L_0073: ldnull 
L_0074: stloc.1 
L_0075: ldstr "Manual collect." 
L_007a: call void [mscorlib]System.Console::WriteLine(string) 
L_007f: nop 
L_0080: call void [mscorlib]System.GC::Collect() 
L_0085: nop 
L_0086: call void [mscorlib]System.GC::WaitForPendingFinalizers() 
L_008b: nop 
L_008c: ldstr "Manual collect." 
L_0091: call void [mscorlib]System.Console::WriteLine(string) 
L_0096: nop 
L_0097: call void [mscorlib]System.GC::Collect() 
L_009c: nop 
L_009d: nop 
L_009e: leave.s L_00aa 
L_00a0: nop 
L_00a1: ldloc.1 
L_00a2: call void [mscorlib]System.Threading.Monitor::Exit(object) 
L_00a7: nop 
L_00a8: nop 
L_00a9: endfinally 
L_00aa: nop 
L_00ab: call string [mscorlib]System.Console::ReadLine() 
L_00b0: pop 
L_00b1: ret 
.try L_0019 to L_0053 finally handler L_0053 to L_0066 
.try L_0072 to L_00a0 finally handler L_00a0 to L_00aa 

no veo ninguna diferencia entre el bloqueo declaración y la Monitor.Enter llamada. Entonces, ¿por qué todavía tengo una referencia a la instancia de test1 en el caso de bloqueo, y el objeto no se recoge por GC, pero en el caso de utilizar Monitor.Enter que se recoge y se finalizó ?

Respuesta

18

Es porque la referencia apuntada por test1 se asigna a la variable local CS$2$0000 en el código IL. Anula la variable test1 en C#, pero la construcción lock se compila de tal manera que se mantiene una referencia separada.

En realidad, es bastante inteligente que el compilador C# hace esto. De lo contrario, se podría eludir la garantía que se supone que la declaración lock obliga a liberar el bloqueo al salir de la sección crítica.

+2

Sip. La declaración de uso funciona de esta manera también. –

77

No veo ninguna diferencia entre la instrucción de bloqueo y la llamada a Monitor.Enter.

Mire con más cuidado. El primer caso copia la referencia a una segunda variable local para garantizar que se mantenga activa.

Aviso lo que la especificación C# 3.0 dice sobre el tema:

Una declaración de bloqueo de la forma "bloqueo (x) ..." donde x es una expresión de un tipo de referencia, es precisamente equivalente a

System.Threading.Monitor.Enter(x); 
try { ... } 
finally { System.Threading.Monitor.Exit(x); } 

excepto que x sólo se evalúa una vez.

Es que el último bit - excepto que x sólo se evalúa una vez - que es la clave para el comportamiento. Para garantizar que x se evalúa solo una vez que lo evaluamos una vez, almacenamos el resultado en una variable local y reutilizamos esa variable local más adelante.

En C# 4 Hemos cambiado la codegen por lo que ahora es

bool entered = false; 
try { 
    System.Threading.Monitor.Enter(x, ref entered); 
    ... 
} 
finally { if (entered) System.Threading.Monitor.Exit(x); } 

pero de nuevo, x es solamente evaluaron vez. En su programa, está evaluando la expresión de bloqueo dos veces. Su código realmente debería ser

bool lockTaken = false; 
    var temp = test2; 
    try { 
     System.Threading.Monitor.Enter(temp, ref lockTaken); 
     test2 = null; 
     Console.WriteLine("Manual collect 3."); 
     GC.Collect(); 
     GC.WaitForPendingFinalizers(); 
     Console.WriteLine("Manual collect 4."); 
     GC.Collect(); 
    } 
    finally { 
     System.Threading.Monitor.Exit(temp); 
    } 

¿Ahora está claro por qué funciona así?

(Nótese también que en C# 4 Introduzca el interior es el intento, no en el exterior como lo fue en C# 3.)

+0

¿Por qué decidiste moverlo dentro del intento en 4.0? –

+11

@Brian: Lee http://blogs.msdn.com/ericlippert/archive/2007/08/17/subtleties-of-c-il-codegen.aspx y luego http://blogs.msdn.com/ericlippert/ archive/2009/03/06/locks-and-exceptions-do-not-mix.aspx –

+0

Sí, está claro ahora, y está mal que yo no haya notado la diferencia. Gracias por la explicación. – Vokinneberg

Cuestiones relacionadas