410 Stimmen

Existieren Zombies ... in .NET?

Ich hatte eine Diskussion mit einem Teamkollegen über das Sperren in .NET. Er ist ein wirklich intelligenter Kerl mit langjähriger Erfahrung sowohl in der nieder- als auch in der höheren Programmierung, aber seine Erfahrung mit der niedrigeren Programmierung übertrifft meine bei weitem. Jedenfalls argumentierte er, dass das Sperren in .NET auf kritischen Systemen, die unter hoher Last stehen, wenn möglich vermieden werden sollte, um die zugegebenermaßen geringe Möglichkeit eines "Zombie-Threads" zu vermeiden, der ein System zum Absturz bringen könnte. Ich benutze regelmäßig das Sperren und wusste nicht, was ein "Zombie-Thread" war, also fragte ich nach. Der Eindruck, den ich aus seiner Erklärung gewonnen habe, ist, dass ein Zombie-Thread ein Thread ist, der beendet wurde, aber auf irgendwie noch einige Ressourcen festhält. Ein Beispiel, das er nannte, wie ein Zombie-Thread ein System zerstören könnte, war ein Thread, der nach dem Sperren eines Objekts einen bestimmten Vorgang startet und dann irgendwann vor der Freigabe des Sperre beendet wird. Diese Situation hat das Potenzial, das System zum Absturz zu bringen, weil schließlich Versuche, diese Methode auszuführen, dazu führen, dass die Threads alle darauf warten, Zugriff auf ein Objekt zu erhalten, das nie zurückgegeben wird, weil der Thread, der das gesperrte Objekt verwendet, tot ist.

Ich denke, ich habe das Wesentliche verstanden, aber falls ich daneben liege, lass es mich bitte wissen. Das Konzept erschien mir logisch. Ich war nicht vollständig überzeugt, dass dies tatsächlich in .NET passieren könnte. Ich habe zuvor nie von "Zombies" gehört, aber ich erkenne an, dass Programmierer, die in tieferen Ebenen gearbeitet haben, ein besseres Verständnis von Rechnergrundlagen (wie dem Threaden) haben. Ich sehe definitiv den Wert des Sperrens, habe aber auch viele erstklassige Programmierer gesehen, die das Sperren nutzen. Ich habe auch nur begrenzte Fähigkeit, dies für mich selbst zu bewerten, weil ich weiß, dass die lock(obj)-Anweisung nur ein syntaktischer Zucker für ist:

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

und weil Monitor.Enter und Monitor.Exit als extern markiert sind. Es scheint vorstellbar, dass .NET eine Art Verarbeitung durchführt, die Threads vor der Exposition gegenüber Systemkomponenten schützt, die solche Auswirkungen haben könnten, aber das ist rein spekulativ und wahrscheinlich nur darauf zurückzuführen, dass ich noch nie von "Zombie-Threads" gehört habe. Daher hoffe ich, hier Feedback dazu zu bekommen:

  1. Gibt es eine klarere Definition eines "Zombie-Threads" als die, die ich hier erklärt habe?
  2. Können Zombie-Threads in .NET auftreten? (Warum/Warum nicht?)
  3. Falls zutreffend, wie könnte ich in .NET die Erstellung eines Zombie-Threads erzwingen?
  4. Falls zutreffend, wie kann ich das Sperren nutzen, ohne ein Szenario mit Zombie-Threads in .NET zu riskieren?

Aktualisierung

Ich habe diese Frage vor etwas mehr als zwei Jahren gestellt. Heute ist dies passiert:

Objekt befindet sich im Zombie-Zustand.

251voto

Justin Punkte 82143
  • Gibt es eine klarere Definition eines "Zombie-Threads" als das, was ich hier erklärt habe?

Scheint mir eine ziemlich gute Erklärung zu sein - ein Thread, der beendet wurde (und daher keine Ressourcen mehr freisetzen kann), dessen Ressourcen (z.B. Handles) jedoch weiter existieren und möglicherweise Probleme verursachen.

  • Können Zombie-Threads in .NET auftreten? (Warum/Warum nicht?)
  • Falls zutreffend, wie könnte ich die Erstellung eines Zombie-Threads in .NET erzwingen?

Aber sicher doch, schau mal, ich habe einen gemacht!

[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);
    }
}

Dieses Programm startet einen Thread Target, der eine Datei öffnet und sich dann sofort mit ExitThread selbst beendet. Der resultierende Zombie-Thread wird den Handle zur Datei "test.txt" niemals freigeben, sodass die Datei geöffnet bleibt, bis das Programm beendet wird (du kannst es mit dem Process Explorer oder ähnlichem überprüfen). Der Handle zu "test.txt" wird nicht freigegeben, bis GC.Collect aufgerufen wird - es ist noch schwieriger als ich dachte, einen Zombie-Thread zu erstellen, der Handles freisetzt)

  • Falls zutreffend, wie kann ich in .NET Sperren nutzen, ohne ein Szenario mit Zombie-Threads zu riskieren?

Mach nicht das, was ich gerade gemacht habe!

Solange dein Code sich ordnungsgemäß aufräumt (verwende Safe Handles oder ähnliche Klassen, wenn du mit nicht verwalteten Ressourcen arbeitest) und solange du nicht außergewöhnliche Methoden zur Beendigung von Threads anwendest (die sicherste Methode besteht darin, Threads nie zu beenden - lass sie sich normal beenden oder durch Ausnahmen, wenn notwendig), wird das einzige, was einem Zombie-Thread ähnelt, nur auftreten, wenn etwas sehr schiefgelaufen ist (zum Beispiel ein Fehler im CLR).

Tatsächlich ist es überraschend schwierig, einen Zombie-Thread zu erstellen (ich musste in eine Funktion P/Invoke, die im Wesentlichen in der Dokumentation sagt, dass man sie nicht außerhalb von C aufrufen soll). Zum Beispiel erstellt der folgende (schreckliche) Code tatsächlich keinen Zombie-Thread.

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

Trotz einiger ziemlich schrecklicher Fehler wird das Handle zu "test.txt" sofort geschlossen, sobald Abort aufgerufen wird (als Teil des Finalisierers für file, der unter der Haube SafeFileHandle verwendet, um seinen Datei-Handle zu umwickeln).

Das Beispiel zur Synchronisierung in C.Evenhuis Antwort ist wahrscheinlich der einfachste Weg, um eine Ressource (in diesem Fall ein Lock) nicht freizugeben, wenn ein Thread in einer nicht ungewöhnlichen Weise beendet wird, aber das lässt sich leicht beheben, indem man entweder eine lock-Anweisung verwendet oder die Freigabe in einem finally-Block platziert.

Siehe auch

47voto

Jerahmeel Punkte 620

Ich habe meine Antwort etwas aufgeräumt, aber die originale Antwort unten zur Referenz gelassen

Es ist das erste Mal, dass ich den Begriff Zombies gehört habe, also gehe ich davon aus, dass dessen Definition ist:

Ein Thread, der beendet wurde, ohne alle seine Ressourcen freizugeben

Also, basierend auf dieser Definition, ja, du kannst das in .NET machen, genauso wie in anderen Sprachen (C/C++, Java).

Allerdings, ich denke nicht, dass dies ein guter Grund ist, um kritischen Code nicht in .NET zu schreiben. Es gibt vielleicht andere Gründe, die gegen .NET sprechen, aber .NET einfach abzulehnen, nur weil es möglicherweise Zombie-Threads geben könnte, macht für mich keinen Sinn. Zombie-Threads sind auch in C/C++ möglich (ich würde sogar argumentieren, dass es in C viel einfacher ist, Fehler zu machen) und viele wichtige, threadbasierte Anwendungen sind in C/C++ (wie Hochfrequenzhandel, Datenbanken etc).

Schlussfolgerung Wenn du gerade dabei bist, eine Sprache für die Verwendung zu wählen, dann empfehle ich, das Gesamtbild zu berücksichtigen: Leistung, Teamfähigkeiten, Zeitplan, Integration mit bestehenden Anwendungen usw. Sicher, Zombie-Threads sind etwas, worüber du nachdenken solltest, aber da es in .NET im Vergleich zu anderen Sprachen wie C so schwierig ist, diesen Fehler tatsächlich zu machen, denke ich, dass diese Sorge von anderen Dingen wie den oben genannten überdeckt wird. Viel Glück!

Ursprüngliche Antwort Zombies† können existieren, wenn du keinen ordnungsgemäßen Thread-Code schreibst. Das gilt auch für andere Sprachen wie C/C++ und Java. Aber das ist kein Grund, keinen Thread-Code in .NET zu schreiben.

Und genauso wie bei jeder anderen Sprache, kenne den Preis, bevor du etwas benutzt. Es ist auch hilfreich zu wissen, was unter der Haube passiert, damit du mögliche Probleme voraussehen kannst.

Zuverlässigen Code für missionkritische Systeme zu schreiben, ist nicht einfach, egal in welcher Sprache du dich befindest. Aber ich bin sicher, dass es möglich ist, dies in .NET richtig zu machen. Auch meines Wissens nach ist das Threading in .NET nicht so verschieden vom Threading in C/C++, es verwendet (oder ist aufgebaut aus) dieselben Systemaufrufe, abgesehen von einigen .NET-spezifischen Konstrukten (wie den leichteren Versionen von RWL- und Event-Klassen).

Das erste Mal, dass ich den Begriff Zombies gehört habe, aber basierend auf deiner Beschreibung, hat dein Kollege wahrscheinlich einen Thread gemeint, der beendet wurde, ohne alle Ressourcen freizugeben. Dies könnte potenziell zu einem Deadlock, Speicherleck oder anderen negativen Nebeneffekten führen. Das ist offensichtlich nicht wünschenswert, aber .NET nur wegen dieser <em>Möglichkeit</em> herauszugreifen, ist wahrscheinlich keine gute Idee, da dies auch in anderen Sprachen möglich ist. Ich würde sogar argumentieren, dass es einfacher ist, in C/C++ Fehler zu machen als in .NET (besonders in C, wo man kein RAII hat), aber viele kritische Anwendungen werden doch in C/C++ geschrieben, oder? Also hängt es wirklich von deinen individuellen Umständen ab. Wenn du jede Geschwindigkeitsreserve aus deiner Anwendung herausholen möchtest und so nah wie möglich am Bare Metal sein möchtest, dann ist .NET <em>möglicherweise</em> nicht die beste Lösung. Wenn du ein begrenztes Budget hast und viel mit Web-Services/bestehenden .NET-Bibliotheken/etc. arbeitest, dann <em>könnte</em> .NET eine gute Wahl sein.

27voto

C.Evenhuis Punkte 25310

Im Moment wurde der Großteil meiner Antwort von den Kommentaren unten korrigiert. Ich werde die Antwort nicht löschen , weil ich die Reputation Points brauche, denn die Informationen in den Kommentaren können für die Leser wertvoll sein.

Immortal Blue wies darauf hin, dass in .NET 2.0 und höher finally Blöcke immun gegen Thread-Abbrüche sind. Und wie von Andreas Niedermair kommentiert, könnte es sich hierbei gar nicht um einen tatsächlichen Zombie-Thread handeln, aber das folgende Beispiel zeigt, wie das Abbrechen eines Threads Probleme verursachen kann:

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

Wenn jedoch ein lock() { } Block verwendet wird, wird das finally trotzdem ausgeführt, wenn eine ThreadAbortException auf diese Weise ausgelöst wird.

Die folgenden Informationen sind nur für .NET 1 und .NET 1.1 gültig:

Wenn innerhalb des lock() { } Blocks eine andere Ausnahme auftritt und die ThreadAbortException genau dann eintrifft, wenn der finally Block ausgeführt werden soll, wird das Schloss nicht freigegeben. Wie Sie erwähnt haben, wird der lock() { } Block kompiliert als:

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

Wenn ein anderer Thread Thread.Abort() im generierten finally Block aufruft, wird das Schloss möglicherweise nicht freigegeben.

24voto

JMK Punkte 25981

Dies handelt nicht von Zombie-Threads, aber das Buch "Effective C#" hat einen Abschnitt zur Implementierung von IDisposable (Punkt 17), der von Zombie-Objekten spricht, was ich interessant fand.

Ich empfehle, das Buch selbst zu lesen, aber im Grunde geht es darum, dass Sie in einer Klasse, die IDisposable implementiert oder einen Destruktor enthält, nur Ressourcen freigeben sollten. Wenn Sie hier andere Dinge tun, besteht die Möglichkeit, dass das Objekt nicht vom Garbage Collector freigegeben wird, aber auch in keiner Weise zugänglich ist.

Es gibt ein ähnliches Beispiel wie unten gezeigt:

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

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

Wenn der Destruktor dieses Objekts aufgerufen wird, wird eine Referenz auf sich selbst in der globalen Liste platziert, was bedeutet, dass es für die Dauer des Programms lebendig und im Speicher bleibt, aber nicht zugänglich ist. Dies kann bedeuten, dass Ressourcen (insbesondere nicht verwaltete Ressourcen) möglicherweise nicht vollständig freigegeben werden, was zu verschiedenen möglichen Problemen führen kann.

Ein umfassenderes Beispiel ist unten gezeigt. Wenn die foreach-Schleife erreicht ist, haben Sie 150 Objekte in der Undead-Liste, von denen jedes ein Bild enthält, aber das Bild wurde gelöscht und es tritt eine Ausnahme auf, wenn Sie versuchen, es zu verwenden. In diesem Beispiel erhalte ich eine ArgumentException (Parameter ist ungültig), wenn ich versuche, irgendetwas mit dem Bild zu tun, sei es speichern oder sogar Abmessungen wie Höhe und Breite anzeigen:

class Program
{
    static void Main(string[] args)
    {
        for (var i = 0; i < 150; i++)
        {
            CreateImage();
        }

        GC.Collect();

        //Etwas zu tun, während der GC läuft
        FindPrimeNumber(1000000);

        foreach (var zombie in Zombie.Undead)
        {
            //Das Objekt ist noch zugänglich, das Bild jedoch nicht
            zombie.Image.Save(@"C:\temp\x.png");
        }

        Console.ReadLine();
    }

    //Entnommen von hier
    //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; // um zu überprüfen, ob eine Primzahl gefunden wurde
            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);
    }
}

Ich weiß, dass Sie speziell nach Zombie-Threads gefragt haben, aber der Titel der Frage handelt von Zombies in .NET, und ich wurde daran erinnert und dachte, dass es für andere interessant sein könnte!

21voto

James World Punkte 28351

Bei kritischen Systemen unter schwerer Last ist das Schreiben von lock-freiem Code hauptsächlich aufgrund der Leistungsverbesserungen besser. Schauen Sie sich Dinge wie LMAX an und wie es "mechanisches Mitgefühl" für großartige Diskussionen nutzt. Machen Sie sich jedoch Sorgen um Zombie-Threads? Ich denke, das ist ein Randfall, der nur ein Fehler ist, der ausgebügelt werden muss, und kein guter Grund, Sperre nicht zu verwenden.

Klingt eher so, als ob Ihr Freund einfach nur schick tut und sein Wissen über obskure exotische Terminologie zur Schau stellt! In all der Zeit, in der ich die Leistungslabore bei Microsoft UK geleitet habe, bin ich nie auf eine Instanz dieses Problems in .NET gestoßen.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X