Laden...

Wie funktioniert der Flow von Async und Tasks?

Erstellt von degri2006 vor einem Jahr Letzter Beitrag vor einem Jahr 682 Views
D
degri2006 Themenstarter:in
21 Beiträge seit 2009
vor einem Jahr
Wie funktioniert der Flow von Async und Tasks?

Hallo,
ich habe male eine Frage zu dem untenstehendene Code. Es gibt eine Main- und MethodAAsync-Methode.
Main startet MethodAAsync mit "Task<int> taskA = MethodAAsync();" stellt es in den Hintergrund, kehrt direkt zurück und führt die for-Schleife. Das geschieht parallel zu der Ausführung von MethodAAsync. Mit "await taskA" werden MethodAAsync und Main wieder zusammengeführt. Falls MethodAAsync noch nicht fertig ist, blockiert Main an dieser Stelle bis MethodAAsync fertig ist. Ist das so richtig zusammengefasst?

static void ConsoleWriteLine(string str)
       {
           int threadId = Thread.CurrentThread.ManagedThreadId;
           Console.ForegroundColor = threadId == 1 ? ConsoleColor.White : ConsoleColor.Cyan;
           Console.WriteLine(
              $"{str}{new string(' ', 26 - str.Length)}   Thread {threadId}");
       }

static async Task<int> MethodAAsync()
       {
           for (int i = 0; i < 5; i++)
           {
               ConsoleWriteLine($" A{i}");
               await Task.Delay(100);
           }
           int result = 123;
           ConsoleWriteLine($" A returns result {result}");
           return result;
       }

static async Task Main(string[] args)
       {

           ConsoleWriteLine($"Start Program");

           Task<int> taskA = MethodAAsync();

           for (int i = 0; i < 5; i++)
           {
               ConsoleWriteLine($" B{i}");
               Task.Delay(50).Wait();
           }

           ConsoleWriteLine("Wait for taskA termination");

           await taskA;

           ConsoleWriteLine($"The result of taskA is {taskA.Result}");
           Console.ReadKey();

           return;
        }
126 Beiträge seit 2023
vor einem Jahr

Also zu dieser Zeile

Task<int> taskA = MethodAAsync();

sagst du

Main startet MethodAAsync mit Task<int> taskA = MethodAAsync(); stellt es in den Hintergrund, kehrt direkt zurück und führt die for-Schleife.

Das stimmt so nicht, denn der Rücksprung erfolgt immer erst beim ersten await wenn es auch wirklich etwas zum awaiten gibt. Bei einem await Task.Delay(0) erfolgt kein Rücksprung und bei jedem anderen erledigtem Task.

Das kann man auch sehen, wenn man etwas mehr Logging einbaut:

    static async Task<int> MethodAAsync()
    {
        ConsoleWriteLine("A - ENTER");
        for (int i = 0; i < 5; i++)
        {
            ConsoleWriteLine($" A{i}");
            await Task.Delay(100);
        }

        int result = 123;
        ConsoleWriteLine($"A returns result {result}");
        return result;
    }

    static async Task Main(string[] args)
    {
        ConsoleWriteLine($"Main - ENTER");
        Task<int> taskA = MethodAAsync();
        ConsoleWriteLine("Main - before loop");
        for (int i = 0; i < 5; i++)
        {
            ConsoleWriteLine($" B{i}");
            Task.Delay(50).Wait();
        }

        ConsoleWriteLine("Wait for taskA termination");
        await taskA;
        ConsoleWriteLine($"The result of taskA is {taskA.Result}");
        Console.ReadKey();
        return;
    }

Die Ausgabe sieht dann wie folgt aus

Main - ENTER                 Thread 1 MAIN
A - ENTER                    Thread 1 MethodA
 A0                          Thread 1 MethodA - jetzt kommt der await Task.Delay(100);
Main - before loop           Thread 1 MAIN
 B0                          Thread 1 MAIN
 B1                          Thread 1 MAIN
 A1                          Thread 5 MethodA - ist jetzt in einem anderen Thread-Kontet unterwegs
 B2                          Thread 1
 B3                          Thread 1
 A2                          Thread 7
 B4                          Thread 1
Wait for taskA termination   Thread 1
 A3                          Thread 7
 A4                          Thread 5
A returns result 123         Thread 7
The result of taskA is 123   Thread 7

Hat die Blume einen Knick, war der Schmetterling zu dick.

D
degri2006 Themenstarter:in
21 Beiträge seit 2009
vor einem Jahr

Ok. Danke.
Ich schlussfolgere folgendes: Main ruft MethodAAsync auf und Main ist blockiert. MethodeAAsync führt seine Befehle aus bis zum ersten await in der for-Schleife. Jetzt wird Main entblockiert und arbeitet die for-Schleife ab, MethodeAAsync wacht nach 100 ms auf, blockiert Main und führt ein Console-Befehl aus und legt sich wieder schlafen. Main macht dann weiter. 
Mit anderen Worten die asynchrone Methode, die von Main aufgerufen wird, blockiert Main bis es zu einem await stösst. Wenn es zu einem await kommt, wird Main enblockiert und arbeitet weiter bis der Task in der asynchronen Methode, der mit await aufgerufen wurde, fertig ist.
Ist das so richtig zusammengefasst?

126 Beiträge seit 2023
vor einem Jahr

Main ruft MethodAAsync auf und Main ist blockiert. MethodeAAsync führt seine Befehle aus bis zum ersten await in der for-Schleife. Jetzt wird Main entblockiert und arbeitet die for-Schleife ab

Ja, kann man so sagen

MethodeAAsync wacht nach 100 ms auf, blockiert Main

Nein, definitiv nicht ... du siehst doch an deinem Log, dass beide auf verschiedenen Threads laufeen

und führt ein Console-Befehl aus

hier könnte es zu einer Blockierung kommen, wenn der Console-Befehl nur den Aufruf von einem Thread zulässt. Blockiert wird allerdings auch nur dann, wenn gleichzeitig von unterschiedlichen Thread dieser ausgeführt werden soll. Das ist wie ein Raum in dem nur eine Person Platz hat. Man muss wrten bis der Raum vom Vorgänger verlassen wurde und dann kann man rein. Wenn ich aber nicht rein muss, werde ich auch nicht blockiert.

und legt sich wieder schlafen.
...
Ist das so richtig zusammengefasst?

Nein, noch nicht ganz.

PS: Ich muss gerade an den Witz mit dem Frosch-Forscher denken. Der wo einem Frosch alle Gliedmaßen abschneidet und dann dem Frosch mit lauter Stimme befiehlt zu hüpfen. Als dieser nicht hüpft, schreibt er auf: "Frosch ohne Beine ist taub."

Hat die Blume einen Knick, war der Schmetterling zu dick.

2.080 Beiträge seit 2012
vor einem Jahr

Ich versuche es mal etwas detaillierter:

Jede asynchrone Methode ist eine StateMachine und in sich geschlossen, es wird nichts blockiert.

Wenn Du eine asynchrone Methode aufrufst, wird ein neuer Task erstellt, der erste Schritt der StateMachine ausgeführt und der Task zurückgegeben.
Die StateMachine übergibt dann beim ersten await an den Task (bzw. es wird vorher TaskAwaiter abgerufen) die Kontrolle, damit der die StateMachine informiert, wenn der Task beendet ist. "Informiert" heißt dann, dass die MoveNext-Methode der StateMachine aufgerufen wird, der dann den nächsten Schritt ausführt, also den Code nach dem await bis zum nächsten await.
Wenn der Task bereits beendet war, dann wird die StateMachine synchron informiert, die MoveNext-Methode wird also ganz normal aufgerufen.
Wenn der Task nicht beendet ist, wird die StateMachine erst nach dessen Beendigung informiert, hierfür wir die MoveNext-Methode an den ThreadPool, oder an den aktuellen SynchronizationContext (wenn nicht null) übergeben. Einige Frameworks nutzen den SynchronizationContext, um den Ablauf von asynchronem Code steuern zu können, im Fall von WPF bedeutet das, dass der Code nach einem await nicht an den ThreadPool übergeben wird, sondern an den UI-Thread.
Das geht so lange weiter, bis die Methode beendet ist.

Jeder Code zwsichen den awaits ist also ein einzelner Zustand in der StateMachine.
Und weil es viele awaits gibt und - im Fall des ThreadPools - viele MoveNext-Aufrufe an den ThreadPool übergeben werden, hast Du auch nach jedem await einen anderen Thread - vorausgesetzt, es konnte nicht synchron weiter arbeiten.

Man kann eine asynchrone Methode natürlich auch aufrufen, ohne darauf zu warten, in dem Fall wird trotzdem der erste synchrone Code ausgeführt werden, aber danach ist die aufgerufene Methode dem Anschein nach beendet, das Ergebnis ist der Task. Der Trick dabei ist, dass - je nachdem, wie die Methode entwickelt wurde - eine lang dauernde Aufgabe (z.B. Datei-/Netzwerk-/Datenbank-Operationen, etc.) gestartet werden kann und fleißig im Hintergrund arbeitet, während deine Main-Methode ganz normal weiter läuft. Wenn Du dann auf den Task wartest, ist der im besten Fall schon beendet, oder es dauert nur noch die Hälfte der Zeit bis zur Beendigung. So kann man auch mehrere asynchrone Methoden aufrufen, die dann alle im Hintergrund in eigenen Threads ihre Arbeit verrichten.

Deine asynchrone Main-Methode, die die andere asynchrone Methode aufruft, arbeitet exakt genauso.
Es wird eine StateMachine gesteuert, deine asynchrone Methode gibt einen Task zurück und wenn der fertig ist, wird die Main-StateMachine weitergeführt.
Bei komplexen Anwendungen ist es also eine seeehr lange Kette von StateMachines, die jeweils die nächste asynchrone Methode aufrufen, einen TaskAwaiter von dem Task abrufen, der dann nach Beendigung die eigene MoveNext-Methode aufruft und so weiter.

Daneben gibt's noch die Möglichkeit, synchron auf einen Task zu warten (Result und Wait()), dabei läuft alles genauso ab, nur dass der aufrufende Thread solange tatsächlich blockiert wird, bis der Task beendet ist. 
Das solltest Du aber nur dann tun, wenn Du auch weißt, was Du tust. Z.B. bei WPF führt das sehr leicht zu einem DeadLock.

Daneben gibt es noch andere Möglichkeiten asynchron zu arbeiten, z.B. kann man mit einer TaskCompletionSource einen Task erstellen und von irgendwo anders steuern. Oder man implementiert einen eigenen TaskAwaiter, aber das braucht man normalerweise nicht.


Soviel zum technischen Ablauf - Ich habe diese Details gebraucht, um es wirklich zu begreifen.
Du solltest dich aber trotzdem nochmal selber eingraben, sharplab.io hilft dabei sehr.

Der konkrete Ablauf deines Codes ist also wie folgt:

  • Main: Start Program
  • MethodAAsync aufrufen
  • MethodAAsync läuft bis zum ersten await
  • MethodAAsync ist aus Sicht der Main-Methode beendet
  • MethodAAsync nach dem await wird an den ThreadPool übergeben und läuft dort unabhängig weiter
  • Main: 5x synchron auf Task.Delay() warten (hier wird der Thread blockiert), währenddessen läuft der Rest von MethodAAsync auf dem ThreadPool weiter
  • Main: Wait for taskA termination - bis hier lief die Main-Methode komplett synchron ab, ab jetzt ist der erste Schritt der StateMachine beendet
  • MethodAAsync beendet den Rest der Arbeit, solange passiert in Main nichts - aber es wird nicht blockiert
  • Der Task von MethodAAsync ruft MoveNext der Main-StateMachine auf, wo dann der zweite und letzte Schritt der Main-StateMachine abgearbeitet wird

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.