web-dev-qa-db-fra.com

Les zombies existent-ils ... en .NET?

Je discutais avec un coéquipier de la possibilité de verrouiller .NET. C'est un gars très brillant avec beaucoup d'expérience dans la programmation de niveau inférieur et supérieur, mais son expérience avec la programmation de niveau inférieur dépasse de loin la mienne. Quoi qu'il en soit, il a fait valoir que le verrouillage .NET devrait être évité sur les systèmes critiques soumis à une charge de travail importante, dans la mesure du possible, afin d'éviter la possibilité certes infime qu'un "thread zombie" se bloque. J'utilise régulièrement le verrouillage et je ne savais pas ce qu'est un "fil de zombie", alors j'ai demandé. L’expression que j’ai eue de son explication est qu’un fil de discussion zombie est un fil qui s’est terminé mais qui conserve toujours certaines ressources. Un exemple qu'il a donné sur la manière dont un thread zombie pourrait casser un système est celui où un thread commence une procédure après le verrouillage d'un objet, puis se termine à un moment donné avant que le verrou ne puisse être libéré. Cette situation risque de planter le système, car les tentatives d'exécution de cette méthode entraîneront toutes les unités d'exécution en attente d'accès à un objet qui ne sera jamais renvoyé, car l'unité d'exécution qui utilise l'objet verrouillé est morte.

Je pense que j'ai compris l'essentiel, mais si je ne suis pas à la base, faites-le-moi savoir. Le concept a eu un sens 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", mais je reconnais que les programmeurs qui ont travaillé en profondeur à des niveaux inférieurs ont tendance à avoir une compréhension plus approfondie des principes de base de l'informatique (comme le threading). Cependant, je vois vraiment l'intérêt du verrouillage et j'ai vu de nombreux programmeurs de classe mondiale utiliser le verrouillage. J'ai également une capacité limitée à évaluer cela pour moi-même parce que je sais que l'instruction lock(obj) n'est qu'un 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 fasse un traitement qui protège les threads de l’exposition à des composants système qui pourraient avoir ce genre d’impact, mais c’est purement spéculatif et probablement juste basé sur le fait que je n’ai jamais entendu parler de "threads zombie". avant. Donc, j'espère avoir quelques retours à ce sujet ici:

  1. Existe-t-il une définition plus claire du "fil de zombie" que ce que j'ai expliqué ici?
  2. Les fils de zombies peuvent-ils se produire sur .NET? (Pourquoi pourquoi pas?)
  3. Le cas échéant, comment puis-je forcer la création d'un fil de discussion zombie dans .NET?
  4. Le cas échéant, comment puis-je utiliser le verrouillage sans risquer un scénario de fil de discussion zombie dans .NET?

Mise à jour

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

Object is in a zombie state.

368
smartcaveman
  • Existe-t-il une définition plus claire du "fil de zombie" que ce que j'ai expliqué ici?

Cela me semble être une très bonne explication - un fil qui s'est terminé (et ne peut donc plus libérer de ressources), mais dont les ressources (par exemple, des poignées) sont toujours présentes et (potentiellement) source de problèmes.

  • Est-ce que des threads zombies peuvent se produire sur .NET? (Pourquoi/Pourquoi pas?)
  • Le cas échéant, comment puis-je forcer la création d'un fil de discussion zombie en .NET?

Ils en ont, regarde, j'en ai fabriqué un!

[DllImport("kernel32.dll")]
private static extern void ExitThread(uint dwExitCode);

static void Main(string[] 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 puis se tue immédiatement avec ExitThreadLe fil de discussion zombie résultant ne publiera jamais le descripteur dans le fichier "test.txt". Le fichier restera donc ouvert jusqu'à la fin du programme (vous pouvez vérifier avec Process Explorer ou similaire). Le descripteur de "test.txt" ne sera publié que le GC.Collect s'appelle - il s'avère qu'il est encore plus difficile que je ne le pensais de créer un fil de zombie qui fuit les poignées)

  • Le cas échéant, comment puis-je utiliser le verrouillage sans risquer un scénario de fil de discussion zombie dans .NET?

Ne fais pas ce que je viens de faire!

Tant que votre code 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 tuer les threads de manière bizarre façons merveilleuses (le moyen le plus sûr est de ne jamais tuer les threads - laissez-les se terminer normalement, ou avec des exceptions si nécessaire), la seule façon pour que vous ayez quelque chose qui ressemble à un thread zombie est si quelque chose est parti très faux (par exemple, quelque chose ne va pas dans le CLR).

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

static void Main(string[] 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 assez terribles, le traitement de "test.txt" est toujours fermé dès que Abort est appelé (dans le cadre du finaliseur de file qui, sous les couvertures, utilise SafeFileHandle pour envelopper son descripteur de fichier)

L'exemple de verrouillage dans réponse C.Evenhuis est probablement le moyen le plus simple de ne pas libérer une ressource (un verrou dans ce cas) lorsqu'un thread se termine de manière non étrange, mais qui est facilement corrigé par soit en utilisant une instruction lock à la place, soit en plaçant la publication dans un bloc finally.

Voir également

232
Justin

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. Je suppose donc que sa définition est la suivante:

n 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 d'autres langages (C/C++, Java).

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

Conclusion Si vous êtes en train de choisir une langue à utiliser, je vous suggère de prendre en compte l'ensemble des éléments suivants: performances, compétences d'équipe, planning, intégration à des applications existantes, etc. Vous devriez penser à cela, mais comme il est si difficile de faire cette erreur dans .NET par rapport à d’autres langages comme C, je pense que cette préoccupation sera éclipsée par d’autres choses comme celles mentionnées ci-dessus. Bonne chance!

Réponse originale Zombies peut exister si vous n'écrivez pas le code de threading 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 dans toute autre langue, connaissez le prix avant d’utiliser quelque chose. Il est également utile de savoir ce qui se passe sous le capot afin de pouvoir prévoir tout problème potentiel.

Le 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 que ce n’est pas impossible de fonctionner correctement dans .NET. De plus, AFAIK, le threading .NET n’est pas si différent du threading en C/C++, il utilise (ou est construit à partir de) les mêmes appels système, à l’exception de 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 selon votre description, votre collègue parlait probablement d’un fil qui se terminait sans libérer toutes les ressources. Cela pourrait potentiellement provoquer un blocage, une fuite de mémoire ou un autre effet indésirable. Ceci n’est évidemment pas souhaitable, mais choisir .NET pour cette raison , la possibilité n’est probablement pas une bonne idée car c’est aussi possible dans d’autres langues. Je dirais même qu’il est plus facile de faire des bêtises en C/C++ qu’en .NET (surtout en C où vous n’avez pas de RAII), mais de nombreuses applications critiques sont écrites en C/C++, non? Cela dépend donc 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 "bare metal", alors .NET pourrait ne pas être le meilleur choix. Solution. Si votre budget est serré et que vous faites beaucoup d’interfaçage avec des services Web/des bibliothèques .net existantes/etc, alors .NET peut être un bon choix.

44
Jerahmeel

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

Immortal Blue a souligné que, dans .NET 2.0 et versions supérieures, les blocs finally sont immunisés contre les abandons de threads. Et comme l'a commenté Andreas Niedermair, il ne s'agit peut-être pas d'un fil de discussion zombie, mais l'exemple suivant montre comment l'abandon d'un fil peut entraîner des problèmes:

class Program
{
    static readonly object _lock = new object();

    static void Main(string[] 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, lors de l'utilisation d'un bloc lock() { }, le finally serait toujours exécuté lorsqu'un ThreadAbortException serait déclenché de cette façon.

Il s'avère que les informations suivantes ne sont valides que pour .NET 1 et .NET 1.1:

Si, à l'intérieur du bloc lock() { }, une autre exception se produit et que le ThreadAbortException arrive exactement au moment où le bloc finally doit être exécuté, le verrou n'est pas relâché. 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() à l'intérieur du bloc finally généré, le verrou peut ne pas être libéré.

25
C.Evenhuis

Cela ne concerne pas les threads Zombie, mais le livre Effective C # contient une section sur la mise en œuvre d’IDisposable (élément 17), qui traite des objets Zombie que je pensais que vous pourriez trouver 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 Desructor, la seule chose à faire est de libérer des ressources. Si vous faites autre chose ici, il y a une chance que l'objet ne soit pas récupéré, mais ne soit en aucun cas accessible.

Il donne un exemple similaire à ci-dessous:

internal class Zombie
{
    private static readonly List<Zombie> _undead = new List<Zombie>();

    ~Zombie()
    {
        _undead.Add(this);
    }
}

Lorsque le destructeur de cet objet est appelé, une référence à lui-même est placée dans la liste globale, ce qui signifie qu'il reste en vie et en mémoire pendant toute la durée du programme, mais qu'il n'est pas accessible. Cela peut signifier que des ressources (en particulier des ressources non gérées) peuvent ne pas être entièrement libérées, ce qui peut entraîner toutes sortes de problèmes.

Un exemple plus complet est ci-dessous. Au moment où la boucle foreach est atteinte, vous avez 150 objets dans la liste des morts-vivants contenant chacun une image, mais cette image a été convertie en GC 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 quoi que ce soit avec l'image, que je tente de l'enregistrer ou même d'afficher des dimensions telles que la hauteur et la largeur:

class Program
{
    static void Main(string[] 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<Zombie> Undead = new List<Zombie>();

    public Zombie(Image image)
    {
        Image = image;
    }

    public Image Image { get; private set; }

    ~Zombie()
    {
        Undead.Add(this);
    }
}

Encore une fois, je sais que vous posez une question sur les fils de zombies en particulier, mais le titre de la question concerne les zombies dans .net, ce qui m’a rappelé et que je pensais que d’autres pourraient le trouver intéressant!

22
JMK

Sur des systèmes critiques soumis à une charge importante, l'écriture de code sans verrouillage est préférable, principalement en raison des améliorations de performances. Regardez des trucs du genre LMAX et voyez comment il exploite la "sympathie mécanique" pour de grandes discussions à ce sujet. Vous vous inquiétez des fils de zombies? Je pense que c'est un cas Edge qui n'est qu'un bogue à résoudre, et une raison suffisante pour ne pas utiliser lock.

On dirait plus que votre ami est juste en train de faire semblant et affiche sa connaissance de la terminologie obscure et obscure pour moi! Pendant tout le temps que j'exécutais les laboratoires de performance chez Microsoft UK, je n'ai jamais rencontré d'instance de ce problème dans .NET.

20
James World

1.Y a-t-il une définition plus claire d'un "fil zombie" que ce que j'ai expliqué ici?

Je suis d’accord pour dire qu’il existe des "fils de zombies", c’est un terme pour désigner ce qui se passe avec des fils laissés avec des ressources qu’ils ne lâchent pas et qui ne meurent pas complètement, d’où le nom "zombie". l'explication de cette référence est assez juste sur l'argent!

2.Les threads zombies peuvent-ils se produire sur .NET? (Pourquoi pourquoi pas?)

Oui, ils peuvent se produire. C'est une référence, et Windows fait référence à "zombie": MSDN utilise le mot "Zombie" pour les processus/threads morts

Cela se produit fréquemment, c'est une autre histoire, et cela dépend de vos techniques et pratiques de codage. En ce qui vous concerne, vous aimez Thread Locking et le faites depuis un moment, je ne m'inquiéterais même pas de ce scénario.

Et oui, comme @KevinPanko l'a correctement mentionné dans les commentaires, les "fils de zombies" proviennent d'Unix, raison pour laquelle ils sont utilisés dans XCode-ObjectiveC et sont 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 objet qui aurait dû mourir devient un "ZombieObject" pour le débogage à la place du "Zombie Thread" qui pourrait constituer un problème potentiel dans votre code.

3
S.H.

Je peux faire des fils de zombies assez facilement.

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

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

Maintenant, tuer un fil de manière à ce qu'il tienne les verrous est une douleur à l'arrière, mais c'est possible. ExitThread() de l'autre gars fait le travail. Comme il l'a découvert, le gestionnaire de fichiers a été nettoyé par le gc, mais pas un lock autour d'un objet. Mais pourquoi feriez-vous cela?

0
Joshua