web-dev-qa-db-fra.com

Instruction Lock vs méthode Monitor.Enter

Je suppose que c'est un exemple de code intéressant.

Nous avons une classe - appelons-la Test - avec une méthode Finalize. Dans la méthode Main, il y a deux blocs de code où j'utilise une instruction de verrouillage et un appel Monitor.Enter (). De plus, j'ai ici deux instances de la classe Test. L'expérience est assez simple: annulez la variable Test dans le bloc de verrouillage, puis essayez de la collecter manuellement avec l'appel de méthode GC.Collect. Donc, pour voir l'appel Finalize j'appelle la méthode GC.WaitForPendingFinalizers. Tout est très simple, comme vous pouvez le voir.

Par la définition de l'instruction lock, elle est ouverte par le compilateur au bloc try {...} enfin {..}, avec un appel Monitor.Enter à l'intérieur du bloc try et Monitor. Il sort ensuite dans le bloc enfin. J'ai essayé d'implémenter manuellement le bloc try-finally.

Je m'attendais au même comportement dans les deux cas - celui d'utiliser le verrouillage et celui d'utiliser Monitor.Enter. Mais, surprise, surprise, c'est différent, comme vous pouvez le voir ci-dessous:

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

La sortie de cet exemple est:

Collecte manuelle 1. Collecte manuelle 2. Collecte manuelle 3. Finalisation du nom de classe Test2. Collecte manuelle 4. Et exception de référence nulle dans le dernier bloc finalement parce que test2 est une référence nulle.

J'ai été surpris et j'ai démonté mon code en IL. Voici donc le vidage IL de la méthode 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

Je ne vois aucune différence entre l'instruction lock et l'appel Monitor.Enter. Alors, pourquoi ai-je encore une référence à l'instance de test1 dans le cas de lock, et l'objet n'est pas collecté par GC, mais dans le cas de l'utilisation de - Monitor.Enter est-il collecté et finalisé?

42
Vokinneberg

C'est parce que la référence pointée par test1 est affecté à la variable locale CS$2$0000 dans le code IL. Vous annulez le test1 variable en C #, mais la construction lock est compilée de manière à conserver une référence distincte.

Il est en fait assez intelligent que le compilateur C # le fasse. Sinon, il serait possible de contourner la garantie que l'instruction lock est censée appliquer pour libérer le verrou à la sortie de la section critique.

21
Brian Gideon

Je ne vois aucune différence entre l'instruction de verrouillage et l'appel Monitor.Enter.

Regardez plus attentivement. Le premier cas copie la référence à une deuxième variable locale pour s'assurer qu'elle reste vivante.

Remarquez ce que la spécification C # 3.0 dit à ce sujet:

Une instruction lock de la forme "lock (x) ..." où x est une expression d'un type de référence, équivaut précisément à

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

sauf que x n'est évalué qu'une seule fois.

C'est ce dernier bit - sauf que x n'est évalué qu'une seule fois - qui est la clé du comportement. Afin de garantir que x n'est évalué qu'une fois, nous l'évaluons une fois, stockons le résultat dans une variable locale et réutilisons cette variable locale plus tard.

En C # 4, nous avons changé le codegen pour qu'il soit maintenant

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

mais encore une fois, x est seulement évalué une fois. Dans votre programme, vous évaluez l'expression de verrouillage deux fois . Votre code devrait vraiment être

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

Maintenant, est-il clair pourquoi cela fonctionne comme il le fait?

(Notez également qu'en C # 4, l'entrée est à l'intérieur de l'essai, pas à l'extérieur comme c'était en C # 3.)

81
Eric Lippert