Оператор блокировки по сравнению с Монитором. Введите метод

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

У нас есть класс - давайте назовем его Тестом - с Завершить методом. В Основном методе существует два блока кода, где я использую оператор блокировки и Монитор. Введите () вызов. Кроме того, у меня есть два экземпляра Тестового класса здесь. Эксперимент довольно прост: Аннулируйте Тестовую переменную в рамках блокировки блока и затем попытайтесь собрать его вручную с вызовом метода GC.Collect. Так, для наблюдения Завершить вызова, я называю метод GC.WaitForPendingFinalizers. Все очень просто, как Вы видите.

По определению оператора блокировки это открыто компилятором попытке {...} Наконец {..} Блок, с Монитором. Введите вызов в блоке попытки и Мониторе. Затем это выходит в наконец блок. Я попытался реализовать попытку наконец блок вручную.

Я ожидал то же поведение в обоих случаях - поведение использования блокировки и поведения использования Монитора. Войти. Но, удивление, удивление, это отличается, как Вы видите ниже:

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 Основного метода:

.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

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

41
задан Community 10 August 2016 в 20:01
поделиться

2 ответа

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

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

21
ответ дан 27 November 2019 в 00:20
поделиться

Я не вижу разницы между оператором блокировки и вызовом 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 - это внутри try, а не снаружи, как это было в C # 3.)

81
ответ дан 27 November 2019 в 00:20
поделиться
Другие вопросы по тегам:

Похожие вопросы: