Оператор блокировки против метода Monitor.Enter

Полагаю, что это интересный пример кода.

У нас есть класс - назовем его Test - с методом Finalize. В методе Main есть два блока кода, в которых я использую оператор блокировки и вызов Monitor.Enter (). Кроме того, здесь у меня есть два экземпляра класса Test. Эксперимент довольно прост: обнулить переменную Test в блоке блокировки, а затем попытаться собрать ее вручную с помощью вызова метода GC.Collect. Итак, чтобы увидеть вызов Finalize, я вызываю метод GC.WaitForPendingFinalizers. Как видите, все очень просто.

По определению оператора lock он открывается компилятором в блоке try {...} finally {..} с вызов Monitor.Enter внутри блока try и Monitor. Затем он выходит в блоке 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. И исключение нулевой ссылки в последнем блоке finally, потому что 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 в случае блокировки, и объект не собирается 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

Я не вижу разницы между оператором блокировки и вызовом Monitor.Enter.

Смотри внимательнее. В первом случае ссылка копируется на вторую локальную переменную, чтобы гарантировать ее сохранение.

Обратите внимание, что по этому поводу говорится в спецификации C # 3.0:

Оператор блокировки в форме «lock (x) ...», где x является выражением ссылочного типа, в точности эквивалентен

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

за исключением того, что x оценивается только один раз.

Последний бит - за исключением того, что x оценивается только один раз - это ключ к поведению. Чтобы гарантировать, что x вычисляется только один раз, мы оцениваем его один раз, сохраняем результат в локальной переменной и повторно используем эту локальную переменную позже.

В C # 4 мы изменили кодогенератор, так что теперь он

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
Почему вы решили переместить его внутрь try в 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, чтобы, если конструктор выдает исключение, частично построенный объект может быть удален (естественно, Dispose должен знать, что объект может быть построен не полностью, но во многих случаях это не должно быть слишком сложно). - person supercat; 10.08.2011