C # async / erwartet seltsames Verhalten in der Konsolen-App

8

Ich habe eine asynchrone Demo-Konsolen-App erstellt und ein seltsames Ergebnis erzielt. Code:

%Vor%

Das Ergebnis unten:

Vielleicht verstehe ich nicht ganz, was genau auf mich wartet. Ich dachte, wenn die Ausführung wartet, dann kehrt die Ausführung zu der Aufrufermethode und Aufgabe zum Warten auf Läufe in einem anderen Thread zurück. In meinem Beispiel werden alle Operationen in einem Thread ausgeführt und die Ausführung kehrt nach dem await-Schlüsselwort nicht zur Aufrufermethode zurück. Wo ist die Wahrheit?

    
Jeksonic 22.07.2015, 13:12
quelle

4 Antworten

2

So funktioniert async-await nicht.

Das Markieren einer Methode als async erzeugt keine Hintergrundthreads. Wenn Sie eine async -Methode aufrufen, wird sie synchron bis zu einem asynchronen Punkt ausgeführt und kehrt dann nur zum Aufrufer zurück.

Dieser asynchrone Punkt liegt vor, wenn Sie await eine Aufgabe noch nicht abgeschlossen haben. Wenn dies abgeschlossen ist, wird der Rest der Methode ausgeführt. Diese Task sollte eine tatsächliche asynchrone Operation darstellen (wie I / O oder Task.Delay ).

In Ihrem Code gibt es keinen asynchronen Punkt, es gibt keinen Punkt, an dem der aufrufende Thread zurückgegeben wird. Der Thread wird immer tiefer und blockiert auf Thread.Sleep , bis diese Methoden abgeschlossen sind und DoAsync zurückgibt.

Nehmen Sie dieses einfache Beispiel:

%Vor%

Hier haben wir einen tatsächlichen asynchronen Punkt ( Task.Delay ), der aufrufende Thread kehrt zu Main zurück und blockiert dann synchron für die Aufgabe. Nach einer Sekunde ist die Task.Delay Aufgabe abgeschlossen und der Rest der Methode wird auf einem anderen ThreadPool -Thread ausgeführt.

Wenn wir statt Task.Delay Thread.Sleep benutzt hätten, dann würde alles auf dem selben aufrufenden Thread laufen.

    
i3arnon 22.07.2015, 13:18
quelle
1

Um dieses Verhalten wirklich zu verstehen, müssen Sie zuerst verstehen, was Task ist und was async und await tatsächlich mit Ihrem Code tun.

Task ist die CLR-Darstellung von "einer Aktivität". Es könnte eine Methode sein, die auf einem Worker-Pool-Thread ausgeführt wird. Es könnte eine Operation sein, um Daten aus einer Datenbank über ein Netzwerk abzurufen. Seine generische Natur erlaubt es, viele verschiedene Implementierungen zu kapseln, aber grundsätzlich müssen Sie verstehen, dass es nur "eine Aktivität" bedeutet.

Die Klasse Task gibt Ihnen Möglichkeiten, den Status der Aktivität zu untersuchen: ob sie abgeschlossen wurde, ob sie noch nicht gestartet wurde, ob sie einen Fehler generiert hat usw. Diese Modellierung einer Aktivität ermöglicht es uns, leichter zu komponieren Programme, die als Folge von Aktivitäten und nicht als Folge von Methodenaufrufen erstellt werden.

Betrachten Sie diesen trivialen Code:

%Vor%

Was das bedeutet ist: "Führe die Methode Foo aus und führe die Methode Bar aus. Wenn wir eine Implementierung betrachten, die Task von Foo und Bar zurückgibt, ist die Zusammensetzung dieser Aufrufe unterschiedlich:

%Vor%

Die Bedeutung ist jetzt "Starten Sie eine Aufgabe mit der Methode Foo und warten Sie, bis sie beendet ist, dann starten Sie eine Aufgabe mit der Methode Bar und warten Sie, bis sie beendet ist." Das Aufrufen von Wait() in Task ist selten korrekt - es bewirkt, dass der aktuelle Thread blockiert, bis der Task abgeschlossen ist, und kann unter einigen häufig verwendeten Threading-Modellen Deadlocks verursachen. Stattdessen können wir async und await verwenden. um einen ähnlichen Effekt ohne diesen gefährlichen Anruf zu erreichen.

%Vor%

Das Schlüsselwort async bewirkt, dass die Ausführung Ihrer Methode in Chunks unterteilt wird: Jedes Mal, wenn Sie await schreiben, nimmt sie den folgenden Code und generiert eine "Fortsetzung": eine Methode, die als Task ausgeführt wird nachdem die erwartete Aufgabe abgeschlossen ist.

Dies unterscheidet sich von Wait() , da ein Task nicht mit einem bestimmten Ausführungsmodell verknüpft ist. Wenn der von Task zurückgegebene Foo() einen Aufruf über das Netzwerk darstellt, wird kein Thread blockiert und auf das Ergebnis gewartet. Es wartet ein Task auf den Abschluss der Operation. Wenn die Operation abgeschlossen ist, wird die Task zur Ausführung geplant - dieser Planungsprozess ermöglicht eine Trennung zwischen der Definition der Aktivität und der Methode, mit der sie ausgeführt wird, und ist die Stärke bei der Verwendung von Aufgaben.

So kann die Methode wie folgt zusammengefasst werden:

  • starte die Aufgabe Foo()
  • Wenn diese Aufgabe abgeschlossen ist, starten Sie die Aufgabe Bar
  • Wenn diese Aufgabe abgeschlossen ist, geben Sie an, dass die Methodenaufgabe abgeschlossen wurde

In Ihrer Konsolenanwendung warten Sie nicht auf Task , das die ausstehende E / A-Operation darstellt. Aus diesem Grund wird ein blockierter Thread angezeigt. Es gibt nie eine Möglichkeit, eine Fortsetzung einzurichten, die asynchron ausgeführt wird.

Wir können Ihre LongIOAsync-Methode reparieren, um Ihre lange IO asynchron zu simulieren, indem Sie die Methode Task.Delay() verwenden. Diese Methode gibt eine Task zurück, die nach einer angegebenen Periode abgeschlossen ist. Dies gibt uns die Möglichkeit für eine asynchrone Fortsetzung.

%Vor%     
Paul Turner 22.07.2015 14:27
quelle
0

Kurze Antwort ist, dass LongIOAsync () blockiert. Wenn Sie dies in einem GUI-Programm ausführen, werden Sie sehen, dass die GUI kurzzeitig einfriert - nicht so, wie async / await funktionieren soll. Deshalb fällt das Ganze auseinander.

Sie müssen alle lang laufenden Operationen in eine Aufgabe umbrechen und dann direkt auf diese Aufgabe warten. Nichts sollte dabei blockieren.

    
Harold Xie 22.07.2015 13:40
quelle
0

Die Zeile, die tatsächlich etwas in einem Hintergrund-Thread ausführt, ist

%Vor%

In Ihrem Beispiel warten Sie nicht auf diese Aufgabe, sondern auf die einer TaskCompletionSource

%Vor%

Wenn Sie auf LongIOAsync warten, warten Sie auf die Aufgabe von tcs, die von einem Hintergrundthread in einem Delegat für Task.Run ()

gesetzt wird

Wenden Sie diese Änderung an:

%Vor%

Alternativ könnte man in diesem Fall die von Task.Run () zurückgegebenen TaskCompletionSource ist für Situationen gedacht, in denen Sie die Möglichkeit haben möchten, Ihre Aufgabe als abgeschlossen oder anderweitig zu definieren.

    
eran otzap 22.07.2015 13:35
quelle