42 votes

Instruction Lock vs méthode Monitor.Enter

Je suppose que c'est un intéressant exemple de code. Nous avons une classe, appelons-le Test avec la méthode Finalize. Dans la méthode main il y a deux blocs de code où je suis à l'aide d'une instruction lock et un Moniteur.Enter() de l'appel. Aussi, j'ai deux instances de la classe de Test ici. L'expérience est assez simple: micropression variable de Test au sein de verrouillage de bloc et ensuite essayer de collecter manuellement avec le GC.Recueillir de l'appel de méthode. Alors, pour voir le Finaliser appel, je suis d'appel GC.WaitForPendingFinalizers méthode. Tout est très simple comme vous pouvez le voir.

Par définition de l'instruction lock il est ouvert par le compilateur pour try{...}finally{..} bloc avec le Moniteur.Entrez appeler à l'intérieur du bloc try et le Moniteur. Il se ferme dans le bloc finally. J'ai essayé de mettre en œuvre l'essai, enfin bloquer manuellement.

J'ai prévu le même comportement dans les deux cas. Dans le cas de l'utilisation de la serrure et dans le cas de l'utilisation du Moniteur.Saisissez. 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

Manuel de recueillir 1. Manuel de collecter les 2. Manuel de recueillir 3. La finalisation de la classe nom Test2. Manuel de recueillir 4. Et la référence nulle exception de la dernière, enfin, le bloc parce que test2 est référence null.

J'ai été surpris et démonté mon code en IL. Donc, ici, c'est l'IL de vidage de la méthode Principale.

.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 pas la différence entre instruction lock et le Moniteur.Entrez appel. Alors, pourquoi faire j'ai encore une référence à l'instance de test1 en cas de blocage, et l'objet n'est pas perçue par GC, mais dans le cas de l'utilisation du Moniteur.Entrez l'information est recueillie et finalisé?

81voto

Eric Lippert Points 300275

Je ne vois pas la différence entre instruction lock et le Moniteur.Entrez appel.

Regarder plus attentivement. Le premier cas copies la mention d'une seconde variable locale pour s'assurer qu'il reste en vie.

Remarquez ce que le C# 3.0 dit sur le sujet:

Un verrouillage de l'énoncé de la forme "lock (x) ...", où x est l'expression d'une référence de type, est précisément l'équivalent de

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

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

C'est que le dernier bit -- sauf que x n'est évaluée une fois -- qui est la clé du comportement. Afin de s'assurer que x est évaluée qu'une seule fois, nous évaluons une fois, stocker le résultat dans une variable locale, et de ré-utiliser cette variable locale plus tard.

En C# 4, nous avons changé le codegen de sorte qu'il est 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 n'est évaluée qu'une fois. Dans votre programme, vous êtes l'évaluation de la serrure expression deux fois. Votre code doit être vraiment

    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, il est clair pourquoi cela fonctionne de la manière qu'il le fait?

(Notez aussi qu'en C# 4 l'Entrée est à l'intérieur de l'essayer, non pas à l'extérieur comme il était en C# 3.)

21voto

Brian Gideon Points 26683

C'est parce que la référence pointée par test1 est affectée à la variable locale CS$2$0000 dans le IL code. Vous null l' test1 variable en C#, mais l' lock construire sera compilé dans une telle manière, qu'une référence est maintenue.

Il est en fait assez intelligent que le compilateur C# ne ce. Sinon il serait possible de contourner le gage de l' lock déclaration est censé appliquer de libérer le verrou à la sortie de la section critique.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X