Подтвердить что ты не робот

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

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

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

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

Я был удивлен и разобрал свой код в IL. Итак, вот дамп IL Основной:

.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

Я не вижу никакой разницы между оператором блокировка и вызовом Monitor.Enter. Итак, почему у меня все еще есть ссылка на экземпляр test1 в случае блокировки, и объект не собирается GC, но в случае использования Monitor.Enter собирается и завершается?

4b9b3361

Ответ 1

Это потому, что ссылка, на которую указывает test1, назначается локальной переменной CS$2$0000 в коде IL. Вы отклоняете переменную test1 в С#, но конструктор lock компилируется таким образом, что поддерживается отдельная ссылка.

На самом деле довольно умно, что компилятор С# делает это. В противном случае можно было бы обойти guarentee, утверждение lock должно обеспечивать освобождение блокировки при выходе из критической секции.

Ответ 2

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

Посмотрите внимательнее. Первый случай копирует ссылку на вторую локальную переменную, чтобы обеспечить ее сохранение.

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

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

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

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

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

В С# 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);   
    }  

Теперь ясно, почему это работает так, как это делается?

(Также обратите внимание, что в С# 4 Enter находится внутри try, а не снаружи, как это было в С# 3.)