Ce code se bloque en mode version mais fonctionne correctement en mode débogage


110

Je suis tombé sur cela et je veux connaître la raison de ce comportement en mode débogage et version.

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

25
quelle est exactement la différence de comportement?
Mong Zhu

4
Si c'était java, je suppose que le compilateur ne voit pas les mises à jour de la variable «compile». L'ajout de «volatile» à la déclaration de variable résoudrait ce problème (et en ferait un champ statique).
Sebastian


4
Remarque: c'est pourquoi le développement multithread a des choses comme les mutex et les opérations atomiques. Lorsque vous commencez à utiliser le multithread, vous devez prendre en compte un ensemble remarquable de problèmes de mémoire supplémentaires qui n'étaient pas évidents auparavant. Des outils de synchronisation de thread comme les mutex auraient résolu ce problème.
Cort Ammon

5
@DavidSchwartz: C'est certainement permis . Et il est permis au compilateur, à l'exécution et au processeur de rendre le résultat de ce code différent de ce que vous attendez. En particulier, C # n'est pas autorisé à rendre l'accès booléen non atomique , mais il est autorisé à déplacer les lectures non volatiles vers l'arrière dans le temps. Les doubles, en revanche, n'ont pas une telle restriction d'atomicité; un double lu et écrit sur deux threads différents sans synchronisation est autorisé à être déchiré.
Eric Lippert

Réponses:


149

Je suppose que l'optimiseur est dupé par l'absence de mot-clé «volatile» sur la isCompletevariable.

Bien sûr, vous ne pouvez pas l'ajouter, car c'est une variable locale. Et bien sûr, puisqu'il s'agit d'une variable locale, elle ne devrait pas être du tout nécessaire, car les locaux sont conservés sur pile et ils sont naturellement toujours «frais».

Cependant , après compilation, ce n'est plus une variable locale . Puisqu'il est accédé dans un délégué anonyme, le code est divisé, et il est traduit en une classe d'assistance et un champ de 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;
    }
}

J'imagine maintenant que le compilateur / optimiseur JIT dans un environnement multithread, lors du traitement de la TheHelperclasse, peut réellement mettre en cache la valeur falsedans un registre ou un cadre de pile au début de la Body()méthode, et ne jamais l'actualiser jusqu'à la fin de la méthode. C'est parce qu'il n'y a AUCUNE GARANTIE que le thread et la méthode ne se termineront PAS avant que "= true" ne soit exécuté, donc s'il n'y a aucune garantie, alors pourquoi ne pas le mettre en cache et obtenir l'amélioration des performances de la lecture de l'objet de tas une fois au lieu de le lire à chaque itération.

C'est exactement pourquoi le mot-clé volatileexiste.

Pour cette classe d'aide pour être correct un peu petit peu mieux 1) dans des environnements multi-thread, il doit avoir:

    public volatile bool isComplete = false;

mais, bien sûr, puisqu'il s'agit de code généré automatiquement, vous ne pouvez pas l'ajouter. Une meilleure approche serait d'ajouter des lock()s autour des lectures et des écritures isCompleted, ou d'utiliser d'autres utilitaires de synchronisation ou de threading / tâches prêts à l'emploi au lieu d'essayer de le faire sans système d'exploitation (ce qui ne sera pas du métal nu, puisque c'est C # sur CLR avec GC, JIT et (..)).

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

BTW. Ce sont juste mes suppositions à ce sujet. Je n'ai même pas essayé de le compiler.

BTW. Cela ne semble pas être un bug; c'est plus comme un effet secondaire très obscur. De plus, si j'ai raison, cela peut être un manque de langage - C # devrait permettre de placer un mot-clé «volatile» sur les variables locales qui sont capturées et promues dans les champs membres dans les fermetures.

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


2
@EricLippert: whoa, merci beaucoup d'avoir confirmé ça si vite! Comment pensez-vous, y a-t-il une chance que dans une version future, nous puissions obtenir une volatileoption sur les variables locales de capture à fermeture? J'imagine que cela peut être un peu difficile à traiter par le compilateur ..
quetzalcoatl

7
@quetzalcoatl: Je ne compte pas sur l'ajout de cette fonctionnalité de si tôt. C'est le genre de codage que vous voudriez décourager et non faciliter . Et d'ailleurs, rendre les choses volatiles ne résout pas nécessairement tous les problèmes. Voici un exemple où tout est volatile et le programme est toujours faux; pouvez-vous trouver le bogue? blog.coverity.com/2014/03/26/reordering-optimizations
Eric Lippert

3
Compris. J'abandonne d'essayer de comprendre les optimisations multithreading ... c'est fou comme c'est compliqué.
Entre le

10
@Pikoh: Encore une fois, pensez comme un optimiseur. Vous avez une variable incrémentée mais jamais lue. Une variable qui n'est jamais lue peut être entièrement supprimée.
Eric Lippert

4
@EricLippert maintenant mon esprit a fait un clic. Ce fil a été très instructif, merci beaucoup, vraiment.
Pikoh

82

La réponse de quetzalcoatl est correcte. Pour en éclairer davantage:

Le compilateur C # et la gigue CLR 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 devez d'écrire des programmes multithread qui indiquent le compilateur et la gigue ce que des choses multithread fou que vous faites.

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

Comment résoudre ça? N'écrivez pas de programmes multithread . Le multithreading est incroyablement difficile à obtenir, même pour les experts. Si vous le devez, utilisez les mécanismes les plus élevés pour atteindre votre objectif . La solution ici n'est pas de rendre la variable volatile. La solution ici est d' écrire une tâche annulable et d'utiliser le mécanisme d'annulation de la bibliothèque parallèle de tâches . Laissez le TPL s'inquiéter de la bonne logique de threading et de l'envoi correct de l'annulation à travers les threads.


1
Les commentaires ne sont pas destinés à une discussion approfondie; cette conversation a été déplacée vers le chat .
Madara's Ghost

14

Je me suis attaché au processus en cours et j'ai trouvé (si je ne faisais pas d'erreur, je ne suis pas très habitué à cela) que la Threadméthode se traduit par 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é pour la première fois et jamais actualisé.


8

Pas vraiment une réponse, mais pour éclaircir davantage le problème:

Le problème semble être quand iest déclaré dans le corps lambda et qu'il est uniquement lu dans 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

Mon pari est un compilateur ou une optimisation JIT concernant le igâchis. Quelqu'un de plus intelligent que moi pourra probablement vous éclairer davantage sur la question.

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


1
voir ma réponse, je suis à peu près sûr qu'il s'agit de mot-clé «volatile» qui ne peut pas être ajouté à une variable locale (qui est en fait promu plus tard dans un champ membre dans la fermeture) ..
quetzalcoatl
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.