112 votes

Ce code se bloque en mode "release" mais fonctionne bien en mode "debug".

Je suis tombé sur ce problème et je voudrais connaître la raison de ce comportement en mode debug et release.

public static void Main(string[] args)
{            
   bool isComplete = false;

   var t = new Thread(() =>
   {
       int i = 0;

        while (!isComplete) i += 0;
   });

   t.Start();

   Thread.Sleep(500);
   isComplete = true;
   t.Join();
   Console.WriteLine("complete!");
}

151voto

quetzalcoatl Points 8814

Je suppose que l'optimiseur est trompé par l'absence du mot-clé " volatile " dans le fichier de données. isComplete variable.

Bien sûr, vous ne pouvez pas l'ajouter, car il s'agit d'une variable locale. Et bien sûr, puisque c'est une variable locale, on ne devrait pas en avoir besoin du tout, parce que les variables locales sont conservées sur pile et ils sont naturellement toujours "frais".

Cependant après la compilation, c'est n'est plus une variable locale . Comme on y accède dans un délégué anonyme, le code est divisé, et il est traduit en une classe d'aide et un champ membre, quelque chose comme :

public static void Main(string[] args)
{
    TheHelper hlp = new TheHelper();

    var t = new Thread(hlp.Body);

    t.Start();

    Thread.Sleep(500);
    hlp.isComplete = true;
    t.Join();
    Console.WriteLine("complete!");
}

private class TheHelper
{
    public bool isComplete = false;

    public void Body()
    {
        int i = 0;

        while (!isComplete) i += 0;
    }
}

Je peux imaginer maintenant que le compilateur/optimiseur JIT dans un environnement multithread, lorsqu'il traite TheHelper peut en fait cache la valeur false dans un registre ou une trame de pile au début de l'opération. Body() et ne le rafraîchissez jamais avant la fin de la méthode. C'est parce qu'il n'y a AUCUNE GARANTIE que le thread&méthode ne se terminera PAS avant que le "=true" ne soit exécuté, donc s'il n'y a aucune garantie, alors pourquoi ne pas le mettre en cache et bénéficier de l'augmentation des performances en lisant l'objet tas une fois au lieu de le lire à chaque itération.

C'est exactement pourquoi le mot-clé volatile existe.

Pour que cette classe d'aide soit correct un tout petit peu mieux 1) dans les environnements multithreads, il devrait l'avoir fait :

    public volatile bool isComplete = false;

mais, bien sûr, comme il s'agit d'un code autogénéré, vous ne pouvez pas l'ajouter. Une meilleure approche serait d'ajouter des lock() autour des lectures et des écritures dans isCompleted ou d'utiliser d'autres utilitaires de synchronisation ou de threading/tasking prêts à l'emploi au lieu d'essayer de le faire à nu (ce qui n'est pas le cas). ne sera pas bare-metal, puisque c'est C# sur CLR avec GC, JIT et ( )).

La différence en mode de débogage s'explique probablement par le fait qu'en mode de débogage, de nombreuses optimisations sont exclues, de sorte que vous pouvez déboguer le code que vous voyez à l'écran. Par conséquent, while (!isComplete) n'est pas optimisé pour que vous puissiez y placer un point d'arrêt, et donc isComplete n'est pas mis en cache de manière agressive dans un registre ou une pile au début de la méthode et est lu depuis l'objet sur le tas à chaque itération de la boucle.

BTW. C'est juste mes suppositions sur ça. Je n'ai même pas essayé de le compiler.

BTW. Il ne semble pas s'agir d'un bogue, mais plutôt d'un effet secondaire très obscur. De plus, si j'ai raison, il peut s'agir d'une déficience du langage - C# devrait permettre de placer le mot clé 'volatile' sur les variables locales qui sont capturées et promues en champs membres dans les closures.

1) voir ci-dessous pour un commentaire d'Eric Lippert sur volatile et/ou cet article très intéressant montrant les niveaux de complexité impliqués pour assurer que le code s'appuyant sur volatile es sûr ..uh, bon ..euh, disons OK.

83voto

Eric Lippert Points 300275

En réponse de quetzalcoatl est correcte. Pour en savoir plus :

Le compilateur C# et le CLR jitter sont autorisés à effectuer un grand nombre d'optimisations qui supposent que le thread actuel est le seul thread en cours d'exécution. Si ces optimisations rendent le programme incorrect dans un monde où le thread actuel n'est pas le seul thread en cours d'exécution c'est votre problème . Vous êtes requis pour écrire des programmes multithreads qui indiquent au compilateur et à Jitter ce que vous faites de fou en multithreads.

Dans ce cas particulier, la gigue est autorisée - mais pas obligée - à observer que la variable est inchangée par le corps de la boucle et à en conclure que - puisque, par hypothèse, c'est le seul thread en cours d'exécution - la variable ne changera jamais. Si elle ne change jamais, il faut vérifier la vérité de la variable. une fois pas à chaque fois dans la boucle. Et c'est en fait ce qui se passe.

Comment résoudre ce problème ? N'écrivez pas de programmes multithreads . Le multithreading est incroyablement difficile à maîtriser, même pour les experts. Si vous devez le faire, alors utiliser les mécanismes du plus haut niveau pour atteindre votre objectif . La solution ici n'est pas de rendre la variable volatile. La solution ici est de écrire une tâche annulable et utiliser le mécanisme d'annulation de la bibliothèque parallèle des tâches . Laissez le TPL s'occuper de la logique du threading et de l'envoi correct de l'annulation entre les threads.

14voto

Matteo Umili Points 2384

Je me suis attaché au processus en cours et j'ai constaté (si je ne me suis pas trompé, je n'ai pas beaucoup d'expérience en la matière) que le fichier Thread est traduite en ceci :

debug051:02DE04EB loc_2DE04EB:                            
debug051:02DE04EB test    eax, eax
debug051:02DE04ED jz      short loc_2DE04EB
debug051:02DE04EF pop     ebp
debug051:02DE04F0 retn

eax (qui contient la valeur de isComplete ) est chargée une première fois et n'est jamais rafraîchie.

8voto

InBetween Points 6162

Pas vraiment une réponse, mais pour éclairer un peu plus la question :

Le problème semble être lorsque i est déclaré à l'intérieur du corps lambda y c'est seulement lire l'expression d'affectation. Sinon, le code fonctionne bien en mode release :

  1. i déclaré en dehors du corps lambda :

    int i = 0; // Declared outside the lambda body
    
    var t = new Thread(() =>
    {
        while (!isComplete) { i += 0; }
    }); // Completes in release mode
  2. i n'est pas lu dans l'expression d'affectation :

    var t = new Thread(() =>
    {
        int i = 0;
        while (!isComplete) { i = 0; }
    }); // Completes in release mode
  3. i est également lu ailleurs :

    var t = new Thread(() =>
    {
        int i = 0;
        while (!isComplete) { Console.WriteLine(i); i += 0; }
    }); // Completes in release mode

Je parie que c'est une optimisation du compilateur ou du JIT concernant i est en train de tout gâcher. Quelqu'un de plus intelligent que moi sera probablement en mesure d'apporter plus de lumière sur cette question.

Néanmoins, je ne m'inquiéterais pas trop à ce sujet, car je ne vois pas où un code similaire pourrait être utile.

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