Изявление за заключване срещу метод Monitor.Enter

Предполагам, че това е интересен пример за код.

Имаме клас -- нека го наречем Test -- с метод Finalize. В метода Main има два кодови блока, където използвам оператор за заключване и извикване на Monitor.Enter(). Освен това имам два екземпляра на класа Test тук. Експериментът е доста прост: Нулирайте променливата Test в блока за заключване и след това опитайте да я съберете ръчно с извикването на метода GC.Collect. И така, за да видя извикването Finalize, извиквам метода GC.WaitForPendingFinalizers. Всичко е много просто, както виждате.

По дефиницията на оператора lock той се отваря от компилатора в блока try{...}finally{..}, с извикване на Monitor.Enter вътре в блока try и Monitor. След това излиза в блоканакрая. Опитах се да внедря ръчно блока try-finally.

Очаквах едно и също поведение и в двата случая -- този при използване на заключване и този при използване на Monitor.Enter. Но изненада, изненада е различно, както можете да видите по-долу:

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

Резултатът от този пример е:

Ръчно събиране 1. Ръчно събиране 2. Ръчно събиране 3. Финализиране на името на класа Test2. Ръчно събиране 4. И изключение за нулева препратка в последния финален блок, защото test2 е нулева препратка.

Бях изненадан и разглобих кода си в IL. И така, ето дъмп на IL на метода 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

Не виждам никаква разлика между израза lock и извикването Monitor.Enter. И така, защо все още имам препратка към екземпляра на test1 в случай на lock и обектът не се събира от GC, а в случай на използване на < strong>Monitor.Enterто е събрано и финализирано?


person Vokinneberg    schedule 14.05.2010    source източник


Отговори (2)


Това е така, защото препратката, към която сочи test1, е присвоена на локалната променлива CS$2$0000 в IL кода. Вие нулирате променливата test1 в C#, но конструкцията lock се компилира по такъв начин, че се поддържа отделна препратка.

Всъщност е доста умно C# компилаторът да прави това. В противен случай би било възможно да се заобиколи гаранцията, която операторът lock трябва да наложи за освобождаване на заключването при излизане от критичната секция.

person Brian Gideon    schedule 14.05.2010

Не виждам никаква разлика между lock statement и Monitor.Enter call.

Погледнете по-внимателно. Първият случай копира препратката към втора локална променлива, за да се гарантира, че тя остава жива.

Забележете какво казва спецификацията на C# 3.0 по темата:

Инструкция за заключване от формата "lock (x) ...", където x е израз на референтен тип, е точно еквивалентна на

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

освен че x се оценява само веднъж.

Именно този последен бит -- с изключение на това, че x се оценява само веднъж -- това е ключът към поведението. За да гарантираме, че x се оценява само след като го оценим веднъж, запазете резултата в локална променлива и използвайте повторно тази локална променлива по-късно.

В C# 4 променихме codegen, така че сега е

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

но отново x се оценява само веднъж. Във вашата програма вие оценявате заключващия израз два пъти. Вашият код наистина трябва да бъде

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

Сега ясно ли е защо това работи по този начин?

(Обърнете внимание също, че в C# 4 Enter е вътре в опита, а не отвън, както беше в C# 3.)

person Eric Lippert    schedule 14.05.2010
comment
Защо решихте да го преместите в опита в 4.0? - person Brian Gideon; 15.05.2010
comment
@Brian: Прочетете блогове. msdn.com/ericlippert/archive/2007/08/17/ и след това blogs.msdn.com/ericlippert/archive/2009/03/06/ - person Eric Lippert; 15.05.2010
comment
Да, вече е ясно и е моя грешка, че не видях разликата. Благодаря за пояснението. - person Vokinneberg; 15.05.2010
comment
Чудя се дали .net някога ще предостави еквивалент за създаването на IDisposable обекти (напр. обектът в процес на изграждане да се съхранява в параметър byref, така че ако конструкторът хвърли изключение, частично конструираният обект може да бъде изхвърлен (естествено, Изхвърлянето трябва да е наясно, че обектът може да не е напълно конструиран, но в много случаи това не трябва да е твърде трудно). - person supercat; 10.08.2011