wake-up-neo.com

Mehrere Awaits in einer einzigen Methode

Ich habe eine Methode wie diese:

public static async Task SaveAllAsync()
{
    foreach (var kvp in configurationFileMap)
    {
        using (XmlWriter xmlWriter = XmlWriter.Create(kvp.Value, XML_WRITER_SETTINGS))
        {
            FieldInfo[] allPublicFields = 
                           kvp.Key.GetFields(BindingFlags.Public | BindingFlags.Static);
            await xmlWriter.WriteStartDocumentAsync();
            foreach (FieldInfo fi in allPublicFields)
            {
                await xmlWriter.WriteStartElementAsync("some", "text", "here");
            }
            await xmlWriter.WriteEndDocumentAsync();
        }
    }
}

Aber ich habe Schwierigkeiten zu verfolgen, was passiert, wenn jemand SaveAllAsync() anruft. 

Was ich denke, wird folgendes passieren:

  1. Wenn jemand das erste Mal anruft, gibt SaveAllAsync() die Kontrolle an den Anrufer an der Leitung await xmlWriter.WriteStartDocumentAsync(); zurück.
  2. Dann ... Wenn sie auf SaveAllAsync() warten (oder auf die Aufgabe warten) ... Was passiert? Wird SaveAllAsync() beim ersten Warten bis zum Aufruf festgehalten? Da es kein Threading gibt, denke ich, ist das der Fall ...
36
Xenoprimate

Sie können sich await als "pausieren" der async-Methode vorstellen, bis der Vorgang abgeschlossen ist. Wenn die Operation bereits abgeschlossen ist (oder extrem schnell ist), wird die Methode await als Sonderfall nicht "pausieren". Die Ausführung wird sofort fortgesetzt.

In diesem Fall (unter der Annahme, dass WriteStartDocumentAsync noch nicht abgeschlossen ist), unterbricht await die Methode und gibt dem Aufrufer eine nicht abgeschlossene Aufgabe zurück. Beachten Sie, dass die von einer Task-Methode zurückgegebene async diese Methode darstellt. Wenn die Methode abgeschlossen ist, ist diese Task abgeschlossen.

Schließlich wird WriteStartDocumentAsync abgeschlossen, und der Rest der async-Methode wird so geplant, dass die Ausführung fortgesetzt wird. In diesem Fall wird der nächste Teil der Methode ausgeführt, bis die nächste await wieder anhält, usw. Wenn die async-Methode abgeschlossen ist, wird die Task abgeschlossen, die zur Darstellung dieser Methode zurückgegeben wurde.

Für weitere Informationen habe ich ein async/await Intro in meinem Blog .

41
Stephen Cleary

Stephens Antwort ist natürlich richtig. Hier ist eine andere Möglichkeit, darüber nachzudenken, was helfen könnte.

Die Fortsetzung einer Menge Code ist das, was passiert, nachdem der Code abgeschlossen ist. Wenn Sie eine await treffen, passieren zwei Dinge. Erstens wird die aktuelle Position in der Ausführung zur Fortsetzung der erwarteten Aufgabe. Zweitens verlässt die Steuerung die aktuelle Methode und einige andere Codeläufe. Der andere Code ist vielleicht die Fortsetzung des ersten Anrufs oder vielleicht etwas ganz anderes, etwa ein Event-Handler.

Aber wenn der Aufruf von xmlWriter.WriteStartDocumentAsync() abgeschlossen ist; was geschieht? Wird die aktuelle Ausführung unterbrochen und an SaveAllAsync() zurückgegeben? 

Es ist nicht klar, was Sie unter dem Aufruf "Abgeschlossen" verstehen. WriteStartDocumentAsync startet ein asynchrones Schreiben, wahrscheinlich in einem E/A-Fertigstellungsthread, und gibt eine Task zurück, die diese asynchrone Arbeit darstellt. Auf diese Aufgabe zu warten hat zwei Dinge, wie ich schon sagte. Zunächst wird die Fortsetzung dieser Aufgabe zur aktuellen Position des Codes. Zweitens verlässt die Steuerung die aktuelle Methode und einige andere Codeläufe. In diesem Fall führt der Code SaveAllAsync die Fortsetzung dieses Aufrufs aus.

Nehmen wir nun an, dass der Code - der Aufrufer von SaveAllAsync - ausgeführt wird, und nimmt an, dass Sie sich in einer Anwendung mit einem UI-Thread befinden, wie z. B. einer Windows Forms-Anwendung oder einer WPF-Anwendung. Jetzt haben wir zwei Threads: den UI-Thread und einen IO Completion-Thread. Der UI-Thread führt den Aufrufer von SaveAllAsync aus, der schließlich zurückkehrt. Jetzt befindet sich der UI-Thread nur noch in einer Schleife und verarbeitet Windows-Meldungen, um Event-Handler auszulösen. 

Schließlich wird der IO beendet und der Fertigstellungsthread IO sendet eine Notiz an den UI-Thread, in der es heißt "Sie können jetzt die Fortsetzung dieser Task ausführen". Wenn der UI-Thread belegt ist, wird diese Nachricht in die Warteschlange gestellt. Schließlich greift der UI-Thread ein und ruft die Fortsetzung auf. Die Steuerung wird nach der ersten await fortgesetzt und Sie treten in die Schleife ein.

Jetzt wird WriteStartElementAsync aufgerufen. Es startet erneut einen Code, der davon abhängt, dass etwas im Fertigstellungsthread IO passiert (vermutlich; wie er seine Arbeit erledigt, hängt davon ab, aber dies ist eine vernünftige Annahme), die eine Task zurückgibt, die diese Arbeit repräsentiert, und Der UI-Thread erwartet diese Aufgabe. Wieder wird die aktuelle Position in der Ausführung angemeldet, wenn die Fortsetzung dieser Aufgabe fortgesetzt wird und die Steuerung an den Aufrufer zurückkehrt, der die erste Fortsetzung aufgerufen hat, nämlich den Ereignisprozessor des UI-Threads. Es verarbeitet fröhlich Nachrichten, bis der IO -Thread dies eines Tages signalisiert und sagt, hey, die angeforderte Arbeit wird im IO Completionsthread erledigt. Bitte rufen Sie die Fortsetzung dieser Aufgabe auf, und so werden wir wieder um die Schleife gehen ...

Sinn ergeben? 

22
Eric Lippert

Immer, wenn eine Funktion "async" ist, bedeutet dies, dass ein "waitit" auf einem System.Threading.Tasks.Task ausgeführt wird.

  1. Die aktuelle Position in der Ausführung wird zu einer "Fortsetzung" der erwarteten Task. Das bedeutet, dass sie nach Abschluss der Task alles Notwendige tut, um sicherzustellen, dass der Rest der async-Methode aufgerufen wird. Diese Arbeit kann in einem bestimmten Thread, einem zufälligen Thread-Pool-Thread oder möglicherweise dem UI-Thread ausgeführt werden. Dies hängt von dem SynchronizationContext ab, den die Task für ihre "Fortsetzung" erhält.

  2. Die Steuerung wird zurückgegeben an:

    • Wenn dies das erste Warten auf die async-Methode ist, kehrt sie mit einer System.Threading.Tasks.Task, die die async-Methode darstellt, zur ursprünglichen aufrufenden Methode zurück (KEINE der innerhalb der async-Methode erstellten Tasks). Es kann es einfach ignorieren und weitermachen, mit einem Task.Result/Task.Wait () darauf warten (darauf achten, dass Sie den UI-Thread nicht blockieren) oder es sogar abwarten, wenn es selbst eine async-Methode ist.

    • Wenn dies nicht das erste Warten in der async-Methode ist, kehrt es einfach zu dem Handler in dem Thread zurück, der die "Fortsetzung" der letzten erwarteten Task ausgeführt hat.

Also um zu antworten:

Wenn sie auf SaveAllAsync () warten (oder auf die Aufgabe warten) ... Was passiert?

Das Erwarten von saveAllAsync () bewirkt nicht unbedingt, dass es auf einem seiner internen erwartungen hängen bleibt. Dies liegt daran, dass ein Erwarten auf SaveAllAsync () nur auf den Aufrufer der Methode namens SaveAllAsync () zurückgreift, die wie nichts geschehen kann, genauso wie SaveAllAsync () interne Erwartungsprüfungen. Dadurch bleibt der Thread in der Lage, zu einem späteren Zeitpunkt (möglicherweise) auf die Anforderung zu reagieren, um die "Fortsetzung" von SaveAllAsync () 's erstem internen waitit auszuführen: await xmlWriter.WriteStartDocumentAsync ()'. Auf diese Weise wird SaveAllAsync () nach und nach beendet und nichts bleibt hängen.

ABER ... wenn Sie OR ein anderer tiefer liegender Code eine Task.Result/Task.Wait () auf eine der durch ein wait zurückgegebenen Tasks ausführt, kann dies dazu führen, dass die Dinge beim "Fortfahren" hängen bleiben "versucht, in demselben Thread wie der wartende Code auszuführen.

0
simon hearn