2010-05-14 10 views
37

Ich nehme an, dass dies ein interessantes Codebeispiel ist.Lock-Anweisung vs Monitor.Enter Methode

Wir haben eine Klasse - nennen wir es -Test - mit einem Finalisierung Methode. In der Main Methode gibt es zwei Codeblöcke, wo ich eine Lock-Anweisung und eine Monitor.Enter() Aufruf verwenden. Außerdem habe ich zwei Instanzen der Test Klasse hier. Das Experiment ist ziemlich einfach: Null die Test Variable innerhalb des Sperrblocks und dann versuchen, es manuell mit dem GC.Collect Methodenaufruf zu sammeln. So, um zu sehen, Anruf Ich rufe die GC.WaitForPendingFinalizers Methode. Alles ist sehr einfach, wie Sie sehen können.

Durch die Definition der Sperre Aussage, es vom Compiler zum versucht geöffnet {...} schließlich {..} Block mit einem Monitor.Enter Anruf innerhalb der Versuchen Sie Block und Monitor. Dann geht es in den schließlich Block. Ich habe versucht, den try-finally Block manuell zu implementieren.

Ich habe das gleiche Verhalten in beiden Fällen erwartet - das Verwenden der Sperre und das Verwenden Monitor.Enter. Aber was für eine Überraschung ist es anders, wie Sie unten sehen können:

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(); 
    } 
} 

Die Ausgabe dieses Beispiels ist:

Handbuch sammeln 1. Hand sammeln 2. Handbuch 3. Finalisierung Klasse sammeln Name Test2. Manual collect 4. Und NULL-Referenzausnahme im letzten finally-Block, da test2 NULL-Referenz ist.

Ich war überrascht und zerlegte meinen Code in IL. So, hier ist der IL-Dump von Haupt Methode:

.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 

Ich sehe keinen Unterschied zwischen der Schloss Anweisung und dem Monitor.Enter Anruf. So , warum habe ich immer noch einen Verweis auf die Instanz von test1 im Fall von Schloss, und das Objekt wird nicht durch GC gesammelt, aber im Fall der Verwendung von Monitor.Enter es wird gesammelt und finalisiert ?

Antwort

18

Dies liegt daran, dass die Referenz, auf die test1 verweist, der lokalen Variablen CS$2$0000 im IL-Code zugewiesen ist. Sie heben die test1 Variable in C# auf, aber das lock Konstrukt wird so kompiliert, dass eine separate Referenz beibehalten wird.

Es ist eigentlich ziemlich clever, dass der C# -Compiler dies tut. Andernfalls wäre es möglich, die Garantie zu umgehen, die die Anweisung lock erzwingen soll, die Sperre beim Verlassen des kritischen Abschnitts zu lösen.

+2

Yup. Die using-Anweisung funktioniert auch so. –

77

Ich sehe keinen Unterschied zwischen Lock-Anweisung und Monitor.Enter Anruf.

Schauen Sie genauer hin. Der erste Fall kopiert den Verweis auf eine zweite lokale Variable, um sicherzustellen, dass sie am Leben bleibt.

bemerken, was die C# 3.0 spec auf dem Thema sagt:

Eine Verriegelungs Anweisung der Form "lock (x), ...", wobei x ist ein Ausdruck eines Referenz-Typ, ist genau äquivalent zu

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

der Ausnahme, dass x nur einmal ausgewertet.

Es ist das letzte Bit - der Ausnahme, dass x nur einmal ausgewertet wird - das ist der Schlüssel zum Verhalten. Um sicherzustellen, dass x nur einmal ausgewertet wird, wenn wir es einmal ausgewertet haben, speichern Sie das Ergebnis in einer lokalen Variablen und verwenden Sie diese lokale Variable später erneut.

In C# 4 wir die codegen geändert haben, so dass es jetzt ist

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

aber wieder, x ist nur einmal ausgewertet. In Ihrem Programm bewerten Sie den Sperrausdruck zweimal. Ihr Code sollte wirklich sein

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); 
    } 

Jetzt ist es klar, warum das so funktioniert, wie es funktioniert?

(Beachten Sie auch, dass in C# 4 die Enter innerhalb der Versuch ist, nicht außerhalb, wie es in C# war 3.)

+0

Warum haben Sie sich entschieden, es in 4.0 in den Versuch zu verschieben? –

+11

@Brian: Lesen Sie http://blogs.msdn.com/ericlippert/archive/2007/08/17/subtilties-of-c-il-codegen.aspx und dann http://blogs.msdn.com/ericlippert/ archiv/2009/03/06/locks-and-exceptions-do-not-mix.aspx –

+0

Ja, es ist jetzt klar, und es ist mein Fehler, ich habe den Unterschied nicht selbst gesehen. Danke für die Erklärung. – Vokinneberg

Verwandte Themen