Worauf achten Sie beim Debuggen von Deadlocks?

25

In letzter Zeit habe ich an Projekten gearbeitet, die häufig Threading verwenden. Ich denke, dass ich in Ordnung bin, sie zu entwerfen. Verwenden Sie so oft wie möglich das zustandslose Design, sperren Sie den Zugriff auf alle Ressourcen, die mehr als einen Thread benötigen usw. Meine Erfahrung in der funktionalen Programmierung hat dazu immens beigetragen.

Wenn ich jedoch den Thread-Code anderer Leute lese, bin ich verwirrt. Ich debugge gerade einen Deadlock und da sich der Codierungsstil und das Design von meinem persönlichen Stil unterscheiden, fällt es mir schwer, mögliche Deadlock-Bedingungen zu erkennen.

Worauf achten Sie beim Debuggen von Deadlocks?

Michael K
quelle
Ich frage dies hier anstelle von SO, weil ich allgemeinere Hinweise zum Debuggen von Deadlocks und keine spezifische Antwort auf mein Problem haben möchte.
Michael K
Ich kann mir vorstellen, dass Strategien protokollieren (wie mehrere andere bereits ausgeführt haben) und tatsächlich die Deadlock-Grafik untersuchen, wer auf eine Sperre wartet, die von wem gehalten wird ( einige finden Sie unter stackoverflow.com/questions/3483094/…) Zeiger) und Anmerkungen sperren (siehe clang.llvm.org/docs/ThreadSafetyAnalysis.html ). Auch wenn es sich nicht um Ihren Code handelt, versuchen Sie möglicherweise, den Autor zum Hinzufügen von Anmerkungen zu überreden. Wahrscheinlich finden sie dabei Fehler und beheben diese (möglicherweise auch Ihre).
Don Hatch

Antworten:

23

Wenn die Situation ein echter Deadlock ist (dh zwei Threads halten zwei verschiedene Sperren, aber mindestens ein Thread möchte eine Sperre, die der andere Thread hält), müssen Sie zuerst alle Vorstellungen darüber aufgeben, wie die Threads die Sperrung anordnen. Nimm nichts an. Möglicherweise möchten Sie alle Kommentare aus dem betrachteten Code entfernen, da Sie durch diese Kommentare möglicherweise an etwas glauben, das nicht zutrifft. Es ist schwer genug zu betonen: Nimm nichts an.

Bestimmen Sie anschließend, welche Sperren gehalten werden, während ein Thread versucht, etwas anderes zu sperren. Wenn möglich, stellen Sie sicher, dass ein Thread in umgekehrter Reihenfolge entsperrt wird. Stellen Sie außerdem sicher, dass ein Thread jeweils nur eine Sperre enthält.

Arbeiten Sie die Ausführung eines Threads sorgfältig durch und untersuchen Sie alle Sperrereignisse. Bestimmen Sie bei jeder Sperre, ob ein Thread andere Sperren enthält, und wenn ja, unter welchen Umständen ein anderer Thread, der einen ähnlichen Ausführungspfad ausführt, zum betreffenden Sperrereignis gelangen kann.

Es ist sicherlich möglich, dass Sie das Problem nicht finden, bevor Ihnen die Zeit oder das Geld ausgeht.

Bruce Ediger
quelle
4
+1 Wow, das ist pessimistisch ... ist es nicht die Wahrheit? Es ist selbstverständlich, dass Sie nicht alle Bugs finden können. Danke für die Vorschläge!
Michael K
Bruce, deine Charakterisierung von "real deadlock" ist überraschend für mich. Ich dachte, ein Deadlock zwischen zwei Threads liegt vor, wenn jeder auf eine Sperre wartet, die der andere hält. Ihre Definition scheint auch den Fall zu umfassen, dass ein Thread, während er eine Sperre hält, darauf wartet, eine zweite Sperre abzurufen, die derzeit von einem anderen Thread gehalten wird. Das klingt für mich nicht wie eine Sackgasse. ist es??
Don Hatch
@DonHatch - Ich habe es schlecht formuliert. Die Situation, die Sie beschreiben, ist keine Sackgasse. Ich hatte gehofft, das Chaos des Debuggens einer Situation zu vermitteln, die eine Thread-Halte-Sperre A enthält, und dann zu versuchen, Sperre B zu erlangen, während der Thread, der Sperre B hält, versucht, Sperre A zu erlangen. Vielleicht. Oder vielleicht ist die Situation viel komplizierter. Sie müssen nur sehr offen über die Reihenfolge der Sperrenerfassung sein. Untersuchen Sie alle Annahmen. Vertraue nichts.
Bruce Ediger
+1 mit dem Vorschlag, den Code sorgfältig zu lesen und alle Sperrvorgänge isoliert zu untersuchen. Es ist viel einfacher, ein komplexes Diagramm zu betrachten, indem Sie einen einzelnen Knoten sorgfältig untersuchen, als zu versuchen, das Ganze auf einmal zu sehen. Wie oft habe ich das Problem gefunden, indem ich nur auf den Code gestarrt und verschiedene Szenarien in meinem Kopf ausgeführt habe.
Newtopian
11
  1. Wie andere gesagt haben ... Wenn Sie nützliche Informationen für die Protokollierung erhalten können, versuchen Sie dies zuerst, da dies am einfachsten ist.

  2. Identifizieren Sie die betroffenen Sperren. Ändern Sie alle Mutex / Semaphore, die für immer auf zeitgesteuerte Wartezeiten warten ... etwas lächerlich Langes wie 5 Minuten. Protokollieren Sie den Fehler, wenn das Zeitlimit überschritten wird. Dies weist Sie zumindest in die Richtung einer der mit dem Problem befassten Sperren. Abhängig von der Variabilität des Timings kann es sein, dass Sie nach einigen Läufen Glück haben und beide Schlösser finden. Verwenden Sie Funktionsfehlercodes / -Bedingungen, um eine Pseudo-Stack-Ablaufverfolgung zu protokollieren, nachdem die zeitgesteuerte Wartezeit nicht ermittelt hat, wie Sie überhaupt dorthin gelangt sind. Dies sollte Ihnen helfen, den Thread zu identifizieren, der an dem Problem beteiligt ist.

  3. Sie können auch versuchen, eine Wrapper-Bibliothek um Ihre Mutex- / Semaphor-Services zu erstellen. Verfolgen Sie, welche Threads über jeden Mutex verfügen und welche Threads auf den Mutex warten. Erstellen Sie einen Monitor-Thread, der überprüft, wie lange Threads blockiert wurden. Triggern Sie nach einer angemessenen Zeit und geben Sie die Statusinformationen aus, die Sie verfolgen.

Irgendwann wird eine einfache Überprüfung des alten Codes erforderlich sein.

Pemdas
quelle
6

Der erste Schritt (wie Péter sagt) ist die Protokollierung. Nach meiner Erfahrung ist dies jedoch oft problematisch. Bei starker Parallelverarbeitung ist dies oft nicht möglich. Ich musste etwas Ähnliches einmal mit einem neuronalen Netzwerk debuggen, das 100k Knoten pro Sekunde verarbeitete. Der Fehler trat erst nach mehreren Stunden auf und selbst eine einzige Ausgabezeile verlangsamte die Dinge so sehr, dass es Tage gedauert hätte. Wenn eine Protokollierung möglich ist, konzentrieren Sie sich weniger auf die Daten, sondern mehr auf den Programmfluss, bis Sie wissen, in welchem ​​Teil dies geschieht. Nur eine einfache Zeile am Anfang jeder Funktion und wenn Sie die richtige Funktion finden, teilen Sie diese in kleinere Abschnitte auf.

Eine andere Möglichkeit besteht darin, Teile des Codes und der Daten zu entfernen, um den Fehler zu lokalisieren. Vielleicht schreiben Sie sogar ein kleines Programm, das nur einige der Klassen benötigt und nur die grundlegendsten Tests ausführt (natürlich immer noch in mehreren Threads). Entfernen Sie alle GUI-bezogenen Elemente, z. B. alle Ausgaben zum aktuellen Verarbeitungsstatus. (Ich fand die Benutzeroberfläche oft genug die Quelle des Fehlers)

Versuchen Sie in Ihrem Code, den gesamten logischen Kontrollfluss zwischen dem Initialisieren und dem Aufheben der Sperre zu befolgen. Ein häufiger Fehler könnte darin bestehen, am Anfang einer Funktion zu sperren und am Ende zu entsperren, aber irgendwo dazwischen eine bedingte return-Anweisung zu haben. Ausnahmen könnten ebenfalls die Freigabe verhindern.

thorsten müller
quelle
"Ausnahmen könnten die Freigabe verhindern" -> Ich bedaure Sprachen, die keine Bereichsvariablen haben: /
Matthieu M.
1
@Matthieu: Variablen mit Gültigkeitsbereichen zu haben und sie tatsächlich richtig zu verwenden, kann zwei verschiedene Dinge sein. Und er fragte nach möglichen Problemen im Allgemeinen, ohne eine bestimmte Sprache zu erwähnen. Dies ist also eine Sache, die den Kontrollfluss beeinflussen kann.
thorsten müller
3

Meine besten Freunde waren Print- / Log-Statements an interessanten Stellen im Code. Diese helfen mir normalerweise dabei, besser zu verstehen, was wirklich in der App vor sich geht, ohne das Timing zwischen verschiedenen Threads zu stören, was die Reproduktion des Fehlers verhindern könnte.

Wenn das fehlschlägt, ist meine einzige verbleibende Methode, auf den Code zu starren und zu versuchen, ein mentales Modell der verschiedenen Threads und Interaktionen aufzubauen und über mögliche verrückte Wege nachzudenken, um das zu erreichen, was anscheinend passiert ist :-) Aber ich nicht betrachte mich als einen sehr erfahrenen Deadlock-Slayer. Hoffentlich können andere bessere Ideen geben, von denen ich auch lernen kann :-)

Péter Török
quelle
1
Ich habe heute ein paar tote Schlösser wie dieses getestet. Der Trick bestand darin, pthread_mutex_lock () mit einem Makro zu umbrechen, das die Funktion, die Zeilennummer, den Dateinamen und den Namen der Mutex-Variablen (durch Token) vor und nach dem Erfassen der Sperre ausgibt. Machen Sie dasselbe auch für pthread_mutex_unlock (). Als ich sah, dass mein Thread eingefroren ist, musste ich mir nur die letzten beiden Nachrichten ansehen. Es gab zwei Threads, die versuchten, zu sperren, aber nie fertig wurden! Jetzt muss nur noch ein Mechanismus hinzugefügt werden, um dies zur Laufzeit umzuschalten. :-)
Plumenator
3

Versuchen Sie zunächst, den Autor des Codes zu ermitteln. Er wird wahrscheinlich die Idee haben, was er geschrieben hat. Selbst wenn Sie beide das Problem nicht einfach durch ein Gespräch herausfinden können, können Sie sich zumindest mit ihm zusammensetzen, um den Deadlock-Teil herauszufinden, der viel schneller ist, als wenn Sie seinen Code ohne Hilfe verstehen.

Gelingt dies nicht, wie Péter Török sagte, ist Logging wahrscheinlich der richtige Weg. Meines Wissens hat Debugger in einer Multithreading-Umgebung schlechte Arbeit geleistet. Versuchen Sie herauszufinden, wo sich die Sperre befindet, und ermitteln Sie, auf welche Ressourcen Sie warten und in welchem ​​Zustand die Rennbedingungen vorliegen.

Zekta Chan
quelle
Nein, die Protokollierung ist hier Ihr Feind. Wenn Sie die langsame Protokollierung aktivieren, ändern Sie das Verhalten des Programms so, dass es leicht ist, ein Programm zu erhalten, das bei aktivierter Protokollierung einwandfrei funktioniert, bei deaktivierter Protokollierung jedoch blockiert. Es ist das gleiche Problem, das Sie haben würden, wenn Sie ein Programm auf einer einzelnen CPU anstatt auf einer Multicore-CPU ausführen.
Gbjbaanb
@ gbjbaanb, ich denke, es ist dein Feind ist viel zu hart. Vielleicht ist es richtig zu sagen, dass es dein bester Freund ist, der dich hin und wieder im Stich lässt. Ich stimme einigen anderen Personen auf dieser Seite zu, die sagen, dass die Protokollierung ein guter erster Schritt ist, nachdem die Prüfung des Codes fehlgeschlagen ist - häufig (in der Tat meistens, meiner Erfahrung nach) wird eine einfache Protokollierungsstrategie ermittelt das Problem leicht und du bist fertig. Andernfalls greifen Sie auf jeden Fall auf andere Methoden zurück, aber ich halte es nicht für ratsam, zu vermeiden, das häufig beste Werkzeug für den Job auszuprobieren, nur weil es nicht immer hilfreich ist.
Don Hatch
0

Diese Frage zieht mich an;) Betrachten Sie sich als erstes als glücklich, da Sie das Problem bei jedem Durchgang konsistent reproduzieren konnten. Wenn Sie jedes Mal dieselbe Ausnahme mit demselben Stacktrace erhalten, sollte dies ziemlich einfach sein. Wenn nicht, vertrauen Sie dem Stacktrace nicht so sehr, sondern überwachen Sie lediglich den Zugriff auf die globalen Objekte und deren Statusänderungen während der Ausführung.


quelle
0

Wenn Sie Deadlocks debuggen müssen, sind Sie bereits in Schwierigkeiten. Verwenden Sie Schlösser in der Regel so schnell wie möglich - oder wenn möglich überhaupt nicht. Jegliche Situation, in der Sie eine Sperre aufheben und dann zu nicht-trivialem Code wechseln, sollte vermieden werden.

Das hängt natürlich von Ihrer Programmierumgebung ab, aber Sie sollten sich Dinge wie sequentielle Warteschlangen ansehen, mit denen Sie möglicherweise nur von einem einzigen Thread aus auf eine Ressource zugreifen können.

Und dann gibt es eine alte, aber unfehlbare Strategie: Weisen Sie jeder Sperre eine "Stufe" zu, beginnend mit Stufe 0. Wenn Sie eine Sperre der Stufe 0 nehmen, dürfen Sie keine anderen Sperren verwenden. Nachdem Sie eine Sperre der Stufe 1 erhalten haben, können Sie eine Sperre der Stufe 0 erhalten. Nachdem du ein Level 10 Schloss genommen hast, kannst du Schlösser mit Level 9 oder niedriger nehmen.

Wenn dies nicht möglich ist, müssen Sie den Code korrigieren, da Sie auf Deadlocks stoßen.

gnasher729
quelle