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;
}
Also zu dieser Zeile
Task<int> taskA = MethodAAsync();
sagst du
Main startet
MethodAAsync
mitTask<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 await
en 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.
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?
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.
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:
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.