CLR über C # 4. Ed. - Verwirrt über das Warten auf Task Deadlock

8

Jeffrey Richter wies in seinem Buch "CLR via C #" auf das Beispiel einer möglichen Sackgasse hin, die ich nicht verstehe (Seite 702, begrenzter Absatz).

Das Beispiel ist ein Thread, der Task ausführt und Wait () für diesen Task aufruft. Wenn der Task nicht gestartet wird, sollte der Aufruf Wait () nicht blockiert werden, stattdessen wird der nicht gestartete Task ausgeführt. Wenn eine Sperre eingegeben wird, bevor der Wait () - Aufruf und der Task ebenfalls versuchen, diese Sperre einzugeben, kann dies zu einem Deadlock führen.

Aber die Sperren werden im selben Thread eingegeben, sollte dies in einem Deadlock-Szenario enden?

Der folgende Code erzeugt die erwartete Ausgabe.

%Vor%

Es ist kein Deadlock aufgetreten.

J. Richter schrieb in seinem Buch "CLR via C #" 4. Auflage auf Seite 702:

  

Wenn ein Thread die Methode Wait aufruft, prüft das System, ob die Task, auf die der Thread wartet, ausgeführt wurde. Wenn dies der Fall ist, blockiert der Thread, der Wait aufruft, solange, bis der Task vollständig ausgeführt wurde. Wenn die Task jedoch noch nicht gestartet wurde, kann das System (abhängig vom TaskScheduler) die Task Trask ausführen, indem es den Thread verwendet, der Wait genannt wird. Wenn dies geschieht, blockiert der Thread, der Wait aufruft, nicht; Es führt die Aufgabe aus und kehrt sofort zurück. Dies ist insofern vorteilhaft, als kein Thread blockiert wurde, wodurch die Ressourcennutzung reduziert wurde (indem kein Thread zum Ersetzen des blockierten Threads erstellt wurde), während die Leistung verbessert wurde (keine Zeit zum Erstellen eines Threads und kein Kontextwechsel). Es kann aber auch schlecht sein, wenn zum Beispiel Threads eine Thread-Synchronisationssperre vor dem Aufruf von Wait ausgeführt haben und die Task versucht, die gleiche Sperre zu übernehmen, was zu einem Deadlock-Thread führt!

Wenn ich den Absatz richtig verstehe, muss der obige Code in einem Deadlock enden!?

    
embee 02.03.2014, 19:49
quelle

5 Antworten

9

Sie nehmen meine Verwendung des Wortes "Schloss" zu wörtlich. Die C # "lock" -Anweisung (die mein Buch von der Verwendung abschreckt), nutzt intern Monitor.Enter / Exit. Die Monitorsperre ist eine Sperre, die die Thread-Eigentümerschaft Rekursion. Daher kann ein einzelner Thread diese Art von Sperre mehrmals erfolgreich erwerben. Wenn Sie jedoch eine andere Art von Sperre verwenden, z. B. Semaphore (Slim), AutoResetEvent (Slim) oder ReaderWriterLockSlim (ohne Rekursion), tritt ein Deadlock auf, wenn ein einzelner Thread versucht, eine dieser Sperren mehrmals zu erfassen.

    
Jeffrey Richter 07.03.2014, 03:33
quelle
2

In diesem Beispiel handelt es sich um Task-Inlining , ein nicht so seltenes Verhalten des Standard-Task-Schedulers des TPL. Dies führt dazu, dass die Aufgabe in demselben Thread ausgeführt wird, der bereits mit Task.Wait() darauf wartet, und nicht in einem zufälligen Pool-Thread. In diesem Fall gibt es kein Deadlock.

Ändern Sie Ihren Code wie folgt und Sie haben einen Dealock:

%Vor%

Die Inlining-Aufgabe ist nicht deterministisch, es kann passieren oder auch nicht. Sie sollten keine Annahmen machen. Überprüfen Sie Task.Wait und "Inlining" von Stephen Toub für weitere Details .

Aktualisiert , die Sperre wirkt sich nicht auf die Task aus, die hier inlining ist. Ihr Code führt immer noch ohne Deadlock aus, wenn Sie taskToRun.Start() in die Sperre verschieben:

%Vor%

Was das Inlining verursacht, ist der Umstand, dass der Haupt-Thread taskToRun.Wait() direkt nach taskToRun.Start() aufruft. Folgendes passiert hinter der Szene:

  1. taskToRun.Start() reiht die Task zur Ausführung durch den Taskplaner ein, aber es wurde noch kein Pool-Thread zugewiesen.
  2. Im selben Thread überprüft der TPL-Code in taskToRun.Wait() , ob der Task bereits ein Pool-Thread zugewiesen wurde (und nicht) und führt ihn inline auf dem Hauptthread aus. In diesem Fall ist es in Ordnung, das gleiche Schloss zweimal ohne Deadlock zu erwerben.
  3. Es gibt auch einen TPL-Taskplaner-Thread. Wenn dieser Thread eine Chance hat, ausgeführt zu werden, bevor taskToRun.Wait() für den Hauptthread aufgerufen wird, tritt kein Inlining auf und Sie erhalten einen Deadlock. Das Hinzufügen von Thread.Sleep(100) vor Task.Wait() würde dieses Szenario modellieren. Inlining tritt auch nicht auf, wenn Sie nicht Task.Wait() verwenden und stattdessen etwas wie AsyncWaitHandle.WaitOne() oben verwenden.

Was das Zitat betrifft, das Sie zu Ihrer Frage hinzugefügt haben, hängt davon ab, wie Sie es gelesen haben. Eine Sache ist sicher: Die selbe Sperre vom Hauptthread kann in der Aufgabe eingegeben werden, wenn die Aufgabe inline wird, ohne einen Deadlock. Sie können einfach keine Vermutungen machen, dass es werden wird.

    
Noseratio 03.03.2014 08:44
quelle
1

In Ihrem Beispiel tritt kein Deadlock auf, weil der Thread, der die Aufgabe plant, und der Thread, der die Aufgabe ausführt, identisch sind. Wenn Sie den Code so ändern würden, dass Ihre Aufgabe in einem anderen Thread ausgeführt wurde, würde der Deadlock auftreten, weil dann zwei Threads um eine Sperre für dasselbe Objekt konkurrieren würden.

Ihr Beispiel, modifiziert, um einen Deadlock zu erstellen:

%Vor%     
cokeman19 02.03.2014 20:41
quelle
1

Dieser Beispielcode hat zwei Standard Threading-Probleme. Um das zu verstehen, müssen Sie zunächst Thread-Rassen verstehen. Wenn Sie einen Thread starten, können Sie nie davon ausgehen, dass er sofort ausgeführt wird. Sie können auch nicht davon ausgehen, dass der Code innerhalb des Threads zu einem bestimmten Zeitpunkt zu einer bestimmten Anweisung gelangt.

Hier kommt es darauf an, ob die Aufgabe vor dem Hauptthread bei der lock -Anweisung ankommt. Mit anderen Worten, ob es vor dem Code im Hauptthread läuft. Modelliere dies als Pferderennen, der Faden, der das Schloss erworben hat, ist das Pferd, das gewinnt.

Wenn es die Aufgabe ist, die gewinnt, ziemlich häufig auf modernen Maschinen mit mehreren Prozessorkernen oder einem einfachen Programm, das keine anderen Threads aktiv hat (und wahrscheinlich, wenn Sie den Code testen), dann geht nichts schief. Er erwirbt die Sperre und verhindert, dass der Haupt-Thread das gleiche tut, wenn er später bei der Sperr-Anweisung ankommt. Sie sehen also die Konsolenausgabe, die Task wird beendet, der Hauptthread erhält nun die Sperre und der Wait () - Aufruf wird schnell abgeschlossen.

Aber wenn der Thread-Pool bereits mit anderen Threads beschäftigt ist oder wenn der Computer Threads in anderen Programmen ausführt oder wenn Sie Pech haben und Sie eine E-Mail erhalten, während die Task gestartet wird, dann tut der Code in der Task nicht t fange sofort an zu laufen und es ist der Hauptthread, der zuerst das Schloss erworben hat. Die Aufgabe kann jetzt die Sperranweisung nicht mehr eingeben, sodass sie nicht abgeschlossen werden kann. Und der Haupt-Thread kann nicht abgeschlossen werden, Wait () wird nie zurückkehren. Eine tödliche Umarmung, Deadlock genannt.

Deadlock ist relativ einfach zu debuggen, Sie haben die ganze Zeit in der Welt, um einen Debugger anzuhängen und die aktiven Threads zu betrachten, um zu sehen, warum sie blockiert sind. Threading-Race-Bugs sind unglaublich schwer zu debuggen, sie kommen zu selten vor und es kann sehr schwierig sein, durch das Ordering-Problem, das sie verursacht, nachzudenken. Ein gängiger Ansatz zur Diagnose von Thread-Races besteht darin, dem Programm eine Ablaufverfolgung hinzuzufügen, damit Sie die Reihenfolge sehen können. Das ändert das Timing und kann den Bug verschwinden lassen. Viele Programme wurden mit der Verfolgung versandt, weil sie das Problem nicht diagnostizieren konnten:)

    
Hans Passant 03.03.2014 12:07
quelle
0

Danke @ Jeffrey-richter für das Aufzeigen, @ embee gibt es Szenario, wenn wir andere Sperren als Monitor verwenden, als ein einzelner Thread versucht, einige dieser Sperren mehrmals zu erwerben, tritt Deadlock auf. Schauen Sie sich das Beispiel unten an

Der folgende Code erzeugt den erwarteten Deadlock. Es muss keine verschachtelte Aufgabe sein, der Deadlock kann auch ohne Verschachtelung auftreten

%Vor%     
vCillusion 07.08.2017 11:27
quelle