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?
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.
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:
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.
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:
Foo()
Bar
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.
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.
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 wirdWenden 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.
Tags und Links .net c# async-await asynchronous c#-5.0