Les zombies existent-ils… dans .NET?

J’avais une discussion avec un coéquipier sur le locking de .NET. C’est un gars très shiny avec une vaste expérience dans la programmation de niveau inférieur et supérieur, mais son expérience de la programmation de niveau inférieur dépasse de loin la mienne. Quoi qu’il en soit, il a fait valoir que le locking .NET devrait être évité sur les systèmes critiques devant être soumis à une charge excessive, dans la mesure du possible, afin d’éviter la faible possibilité d’un “thread zombie”. J’ai l’habitude d’utiliser le locking et je ne savais pas ce qu’est un “fil zombie”, alors j’ai demandé. L’impression que j’ai tirée de son explication est qu’un thread zombie est un thread qui s’est terminé, mais qui conserve encore certaines ressources. Un exemple qu’il a donné de la façon dont un thread zombie pouvait casser un système était qu’un thread entame une procédure après avoir verrouillé un object, puis se termine à un moment donné avant que le verrou puisse être libéré. Cette situation est susceptible de provoquer le blocage du système, car les tentatives d’exécution de cette méthode entraîneront l’attente des threads à accéder à un object qui ne sera jamais renvoyé, car le thread qui utilise l’object verrouillé est mort.

Je pense que j’ai compris l’essentiel, mais si je suis hors de la base, s’il vous plaît faites le moi savoir. Le concept était logique pour moi. Je n’étais pas complètement convaincu que c’était un scénario réel qui pourrait se produire dans .NET. Je n’ai jamais entendu parler de “zombies” auparavant, mais je reconnais que les programmeurs qui ont travaillé en profondeur à des niveaux inférieurs ont tendance à mieux comprendre les fondamentaux de l’informatique (comme le threading). Je vois vraiment la valeur du locking, cependant, et j’ai vu de nombreux programmeurs de classe mondiale tirer parti du locking. J’ai aussi une capacité limitée à évaluer cela pour moi-même parce que je sais que la déclaration de lock(obj) n’est vraiment que du sucre syntaxique pour:

 bool lockWasTaken = false; var temp = obj; try { Monitor.Enter(temp, ref lockWasTaken); { body } } finally { if (lockWasTaken) Monitor.Exit(temp); } 

et parce que Monitor.Enter et Monitor.Exit sont marqués extern . Il semble concevable que .NET effectue un traitement qui protège les threads de l’exposition aux composants du système qui pourraient avoir ce genre d’impact, mais c’est purement spéculatif et probablement simplement parce que je n’ai jamais entendu parler de “threads zombies”. avant. Donc, j’espère que je peux obtenir des commentaires à ce sujet ici:

  1. Y a-t-il une définition plus claire d’un “fil de zombie” que ce que j’ai expliqué ici?
  2. Les threads zombies peuvent-ils apparaître sur .NET? (Pourquoi pourquoi pas?)
  3. Le cas échéant, comment pourrais-je forcer la création d’un thread zombie dans .NET?
  4. Le cas échéant, comment puis-je exploiter le locking sans risquer un scénario de thread zombie dans .NET?

Mettre à jour

J’ai posé cette question il y a un peu plus de deux ans. Aujourd’hui c’est arrivé:

L'objet est dans un état de zombie.

  • Y a-t-il une définition plus claire d’un “fil de zombie” que ce que j’ai expliqué ici?

Cela semble être une bonne explication pour moi – un thread qui s’est terminé (et ne peut donc plus libérer de ressources), mais dont les ressources (par exemple les descripteurs) sont toujours présentes et (potentiellement) source de problèmes.

  • Les threads zombies peuvent-ils apparaître sur .NET? (Pourquoi pourquoi pas?)
  • Le cas échéant, comment pourrais-je forcer la création d’un thread zombie dans .NET?

Ils sont sûrs, regardez, j’en ai fabriqué un!

 [DllImport("kernel32.dll")] private static extern void ExitThread(uint dwExitCode); static void Main(ssortingng[] args) { new Thread(Target).Start(); Console.ReadLine(); } private static void Target() { using (var file = File.Open("test.txt", FileMode.OpenOrCreate)) { ExitThread(0); } } 

Ce programme démarre un thread Target qui ouvre un fichier et se tue immédiatement à l’aide d’ ExitThread . Le thread zombie résultant ne libèrera jamais le handle du fichier “test.txt” et le fichier restra ouvert jusqu’à la fin du programme (vous pouvez vérifier avec l’explorateur de processus ou similaire). Le handle de “test.txt” ne sera pas publié avant que GC.Collect soit appelé – il s’avère qu’il est encore plus difficile que je pensais de créer un fil de zombie qui fuit des poignées)

  • Le cas échéant, comment puis-je exploiter le locking sans risquer un scénario de thread zombie dans .NET?

Ne fais pas ce que je viens de faire!

Tant que votre code se nettoie correctement après lui-même (utilisez Safe Handles ou des classes équivalentes si vous travaillez avec des ressources non gérées), et tant que vous ne faites pas tout votre possible pour éliminer les threads de manière étrange pour ne jamais tuer les threads – laissez-les se terminer normalement, ou par des exceptions si nécessaire. La seule manière de ressembler à un thread zombie est de savoir si quelque chose ne va pas (par exemple, quelque chose ne va pas dans le CLR).

En fait, il est en fait étonnamment difficile de créer un fil de zombie (je devais invoquer P / Invoke dans une fonction qui vous indique dans la documentation de ne pas l’appeler en dehors de C). Par exemple, le code suivant (horrible) ne crée pas de thread zombie.

 static void Main(ssortingng[] args) { var thread = new Thread(Target); thread.Start(); // Ugh, never call Abort... thread.Abort(); Console.ReadLine(); } private static void Target() { // Ouch, open file which isn't closed... var file = File.Open("test.txt", FileMode.OpenOrCreate); while (true) { Thread.Sleep(1); } GC.KeepAlive(file); } 

Malgré quelques erreurs terribles, le handle de “test.txt” est toujours fermé dès que Abort est appelé (dans le cadre du finaliseur du file qui, sous les couvertures, utilise SafeFileHandle pour envelopper le descripteur de fichier)

L’exemple de locking dans la réponse de C.Evenhuis est probablement le moyen le plus facile de ne pas libérer une ressource (un verrou dans ce cas) lorsqu’un thread se termine d’une manière non-bizarre, mais facilement réparable en utilisant une instruction de lock . ou en mettant la version dans un bloc finally .

Voir également

  • Subtilités de C # IL codegen pour un cas très subtil où une exception peut empêcher la libération d’un verrou, même en utilisant le mot-clé lock (mais uniquement dans .Net 3.5 et les versions antérieures)
  • Les serrures et les exceptions ne se mélangent pas

J’ai un peu nettoyé ma réponse, mais j’ai laissé l’original ci-dessous pour référence

C’est la première fois que j’entends parler de zombies, alors je suppose que sa définition est la suivante:

Un thread qui s’est terminé sans libérer toutes ses ressources

Donc, étant donné cette définition, alors oui, vous pouvez le faire dans .NET, comme avec les autres langages (C / C ++, java).

Cependant , je ne pense pas que ce soit une bonne raison de ne pas écrire de code threadé et critique dans .NET. Il peut y avoir d’autres raisons de décider de ne pas utiliser .NET, mais écrire sur .NET simplement parce que vous pouvez avoir des threads de zombies n’a pas de sens pour moi. Les threads de zombies sont possibles en C / C ++ (je dirais même que c’est beaucoup plus facile de gâcher en C) et beaucoup d’applications critiques en threads sont en C / C ++ (trading à haut volume, bases de données, etc.).

Conclusion Si vous êtes en train de décider d’une langue à utiliser, alors je vous suggère de prendre en compte la grande image: performance, compétences d’équipe, calendrier, intégration avec les applications existantes, etc. , mais comme il est si difficile de faire cette erreur dans .NET par rapport aux autres langages comme le C, je pense que d’autres choses comme celles mentionnées ci-dessus occulteront cette préoccupation. Bonne chance!

Les zombies † d’origine peuvent exister si vous n’écrivez pas le code de thread approprié. La même chose est vraie pour d’autres langages comme C / C ++ et Java. Mais ce n’est pas une raison pour ne pas écrire de code threadé dans .NET.

Et comme avec n’importe quelle autre langue, sachez le prix avant d’utiliser quelque chose. Il est également utile de savoir ce qui se passe sous le capot pour pouvoir prévoir d’éventuels problèmes.

Un code fiable pour les systèmes critiques n’est pas facile à écrire, quelle que soit la langue dans laquelle vous vous trouvez. Mais je suis convaincu qu’il n’est pas impossible de faire correctement dans .NET. Aussi, AFAIK, .NET threading n’est pas si différent de threading en C / C ++, il utilise (ou est construit à partir) des mêmes appels système sauf pour certaines constructions spécifiques à .net (comme les versions allégées de RWL et les classes d’événements).

† La première fois que j’ai entendu parler du terme «zombies», mais d’après votre description, votre collègue voulait probablement dire un thread qui se terminait sans libérer toutes les ressources. Cela pourrait entraîner un blocage, une fuite de mémoire ou un autre effet secondaire indésirable. Ce n’est évidemment pas souhaitable, mais choisir .NET à cause de cette possibilité n’est probablement pas une bonne idée car cela est possible dans d’autres langues aussi. Je dirais même qu’il est plus facile de gâcher en C / C ++ que dans .NET (particulièrement en C où vous n’avez pas de RAII) mais beaucoup d’applications critiques sont écrites en C / C ++, n’est-ce pas? Donc, cela dépend vraiment de votre situation personnelle. Si vous souhaitez extraire chaque once de vitesse de votre application et que vous souhaitez vous rapprocher le plus possible du nu, alors .NET pourrait ne pas être la meilleure solution. Si vous avez un budget serré et que vous faites beaucoup d’interfaçage avec les services Web / les bibliothèques .net existantes, etc., .NET peut être un bon choix.

Actuellement, la plupart de mes réponses ont été corrigées par les commentaires ci-dessous. Je ne supprimerai pas la réponse car j’ai besoin des points de réputation car les informations contenues dans les commentaires peuvent être utiles aux lecteurs.

Immortal Blue a fait remarquer que, dans .NET 2.0 et plus, les blocs résistent aux avortements de threads. Et comme l’a fait remarquer Andreas Niedermair, il ne s’agit peut-être pas d’un thread de zombie, mais l’exemple suivant montre comment l’interruption d’un thread peut poser problème:

 class Program { static readonly object _lock = new object(); static void Main(ssortingng[] args) { Thread thread = new Thread(new ThreadStart(Zombie)); thread.Start(); Thread.Sleep(500); thread.Abort(); Monitor.Enter(_lock); Console.WriteLine("Main entered"); Console.ReadKey(); } static void Zombie() { Monitor.Enter(_lock); Console.WriteLine("Zombie entered"); Thread.Sleep(1000); Monitor.Exit(_lock); Console.WriteLine("Zombie exited"); } } 

Cependant, lorsque vous utilisez un bloc lock() { } , le finally sera toujours exécuté lorsqu’une ThreadAbortException est déclenchée de cette manière.

Les informations suivantes s’avèrent uniquement valides pour .NET 1 et .NET 1.1:

Si, à l’intérieur du bloc lock() { } , une autre exception se produit et que ThreadAbortException arrive exactement lorsque le bloc finally est sur le point d’être exécuté, le verrou n’est pas libéré. Comme vous l’avez mentionné, le bloc lock() { } est compilé comme suit:

 finally { if (lockWasTaken) Monitor.Exit(temp); } 

Si un autre thread appelle Thread.Abort() dans le bloc finally généré, le verrou peut ne pas être libéré.

Il ne s’agit pas des fils de Zombie, mais le livre Effective C # contient une section sur l’implémentation d’IDisposable (item 17), qui traite des objects Zombie que vous pensiez peut-être intéressants.

Je recommande de lire le livre lui-même, mais l’essentiel est que si vous avez une classe implémentant IDisposable, ou contenant un Desctructor, la seule chose que vous devriez faire est de libérer des ressources. Si vous faites autre chose ici, il y a une chance que l’object ne soit pas récupéré, mais qu’il ne soit pas accessible de quelque manière que ce soit.

Il donne un exemple similaire à ci-dessous:

 internal class Zombie { private static readonly List _undead = new List(); ~Zombie() { _undead.Add(this); } } 

Lorsque le destructeur de cet object est appelé, une référence à lui-même est placée sur la liste globale, ce qui signifie qu’il rest actif et en mémoire pour la durée de vie du programme, mais n’est pas accessible. Cela peut signifier que les ressources (en particulier les ressources non managées) peuvent ne pas être entièrement libérées, ce qui peut entraîner toutes sortes de problèmes potentiels.

Un exemple plus complet est ci-dessous. Au moment où la boucle foreach est atteinte, vous avez 150 objects dans la liste Undead contenant chacun une image, mais l’image a été GC’d et vous obtenez une exception si vous essayez de l’utiliser. Dans cet exemple, j’obtiens une exception ArgumentException (le paramètre n’est pas valide) lorsque j’essaie de faire quelque chose avec l’image, que j’essaie de l’enregistrer ou même de visualiser des dimensions telles que la hauteur et la largeur:

 class Program { static void Main(ssortingng[] args) { for (var i = 0; i < 150; i++) { CreateImage(); } GC.Collect(); //Something to do while the GC runs FindPrimeNumber(1000000); foreach (var zombie in Zombie.Undead) { //object is still accessable, image isn't zombie.Image.Save(@"C:\temp\x.png"); } Console.ReadLine(); } //Borrowed from here //http://stackoverflow.com/a/13001749/969613 public static long FindPrimeNumber(int n) { int count = 0; long a = 2; while (count < n) { long b = 2; int prime = 1;// to check if found a prime while (b * b <= a) { if (a % b == 0) { prime = 0; break; } b++; } if (prime > 0) count++; a++; } return (--a); } private static void CreateImage() { var zombie = new Zombie(new Bitmap(@"C:\temp\a.png")); zombie.Image.Save(@"C:\temp\b.png"); } } internal class Zombie { public static readonly List Undead = new List(); public Zombie(Image image) { Image = image; } public Image Image { get; private set; } ~Zombie() { Undead.Add(this); } } 

Encore une fois, je suis conscient que vous posiez des questions sur les fils de zombies en particulier, mais le titre de la question concerne les zombies dans .net, et cela m’a rappelé et pensé que d’autres pourraient le trouver intéressant!

Sur les systèmes critiques soumis à une charge importante, l’écriture de code sans verrou est préférable, principalement grâce aux améliorations de performances. Regardez des choses comme LMAX et comment il tire parti de la «sympathie mécanique» pour de grandes discussions à ce sujet. Vous inquiétez pas des fils de zombie? Je pense que c’est un casse-tête qui n’est qu’un bogue à régler, et pas une raison suffisante pour ne pas utiliser le lock .

Cela ressemble plus à votre ami est juste fantaisie et affichant sa connaissance de la terminologie exotique obscure pour moi! Pendant tout le temps que je dirigeais les laboratoires de performance chez Microsoft UK, je ne suis jamais tombé sur une instance de ce problème dans .NET.

1.Y a-t-il une définition plus claire d’un “fil de zombie” que ce que j’ai expliqué ici?

Je suis d’accord que “Zombie Threads” existe, c’est un terme qui fait référence à ce qui se passe avec les Threads qui sont laissés avec des ressources qu’ils ne lâchent pas et qui ne meurent pas complètement, d’où le nom “zombie”. explication de cette référence est assez juste sur l’argent!

2.Les fils de zombie peuvent-ils apparaître sur .NET? (Pourquoi pourquoi pas?)

Oui, ils peuvent se produire C’est une référence, que Windows appelle “zombie”: MSDN utilise le mot “Zombie” pour les processus / threads Dead.

Si cela se produit souvent, c’est une autre histoire, et cela dépend de vos techniques et pratiques de codage. En ce qui vous concerne, comme Thread Locking et vous l’avez fait pendant un certain temps, je ne m’inquiéterais même pas.

Et oui, comme @KevinPanko correctement mentionné dans les commentaires, “Zombie Threads” provient d’Unix, c’est pourquoi ils sont utilisés dans XCode-ObjectiveC et appelés “NSZombie” et utilisés pour le débogage. Il se comporte à peu près de la même manière … la seule différence est qu’un object qui aurait dû mourir devient un “ZombieObject” pour le débogage à la place du “Zombie Thread” qui pourrait être un problème potentiel dans votre code.

Je peux facilement créer des fils de zombie.

 var zombies = new List(); while(true) { var th = new Thread(()=>{}); th.Start(); zombies.Add(th); } 

Cela fuit les poignées de fil (pour Join() ). C’est juste une autre fuite de mémoire en ce qui nous concerne dans le monde géré.

Alors, tuer un fil de manière à ce qu’il tienne réellement des serrures est une douleur à l’arrière mais possible. ExitThread() autre gars fait le travail. Comme il a trouvé, le handle de fichier a été nettoyé par le gc mais un lock autour d’un object ne le ferait pas. Mais pourquoi tu ferais ça?