Ein promise Task repräsentiert eine IO-bound Aufgabe die während des Ausführens der IO-bound Aufgabe kein Thread belegt. Je nach Einstellung vom await-Statement (ConfigureAwait()) und dem vorhanden sein eines Synchronisationcontexts wird der Code nach dem await-Statement auf einem Threadpool Thread ausgeführt oder umgeleitet auf den aufrufenden Thread. Dieser Blog erklärt den genauen Ablauf des async\await-Konstrukts.
Der Ablauf ist kompliziert, muss aber verstanden werden, um fehlerfrei arbeiten zu können. Im Folgenden wird der Einsatz von async\await gezeigt, anschliessend der Thread-Ablauf und schlussendlich, was unter der Haube passiert.
Die meisten Programmierer kennen nur diese Sicht. Dies reicht allerdings nicht aus um fehlerfreien Code zu schreiben.
Schauen wir uns mal folgendes Beispiel von aussen an.
Wichtig ist der Name der Methode StartAsyncAwaitSimpleAsync. Der Name endet mit dem Wort Async. Das ist eine Etikette, eine Best Practice und sagt dem Verwender «diese Methode ist asynchron» oder in anderen Worten: ‚Diese Methode gibt den aufrufenden Thread frei während des Wartens‘.
Dies hat mit Etikette und Sauberkeit zu tun. Sobald irgendwo in der Aufrufkette ein Task.Run() vorkommt, ist gegen diese Regel verstosen und der Methodenname ‚lügt‘.
Die Signatur asynchroner Methoden endet immer auf Async. Die Methode darf keine Threads starten.
Bis am Aufruf Task.Delay:
Der UI Thread kann jetzt andere Arbeiten erledigen: die Applikation ist Skalierbarer weil kein Thread beansprucht wird während des Wartens.
Da wo die rote Pfeile sind wird werden die Methoden pausiert und später wieder ausgeführt.
Der System Timer Task.Delay läuft ab:
Mit ConfigureAwait(false) teilt man dem Compiler mit, dass es egal ist, auf welchem Thread der Code nach dem await abläuft. Der Scheduler muss dann nicht den SynchronizationContext speichern vor dem await und nach dem await muss der Scheduler nicht den Aufruf SynchronizationContext.Post() machen um den Code nach dem await auf dem gleichen Thread ausführen zu lassen als vor dem await. Das spart Zeit.
Wenn immer möglich ConfigureAwait(false) anwenden.
Bis am Aufruf Task.Delay:
Da wo die rote Pfeile sind wird werden die Methoden pausiert und später wieder ausgeführt.
Der System Timer Task.Delay läuft ab:
Der UI Thread ist während des zweiten Teils immer frei für andere Arbeiten. Aber, der zweite Teil von BtnStartAsyncAwaitSimple_Click() darf jetzt nicht auf UI Controls zugreifen weil dieser Teil nicht auf dem UI Thread läuft.
Damit der restliche Code vom Click Handler auf dem UI Thread ausgeführt wird, kann man ConfigureAwait(false) vom äusseren await auf true setzen; der default Wert. Das entspricht also dem Code im ersten Beispiel.
Der Rest vom StartAsyncAwaitSimpleAsync() läuft immer noch auf einem Threadpool Thread. Das await vom BtnStartAsyncAwaitSimple_Click() hat aber vorher den Synchronisation Context gespeichert und verwendet diesen nun um den Aufruf auf den UI Thread umzuleiten.
Wie vorher erklärt passiert das über die Windows Message Queue und das ist der Grund wieso blockierende Aufrufe wie Result() oder Wait() zu Deadlocks führen können. Aber ein await Statement ist nicht blockierend und der Rest vom BtnStartAsyncAwaitSimple_Click() wird auf dem UI Thread ausgeführt.
Unter der Haube generiert das .NET Framework für eine async Methode eine Zustandsmaschine (MethodAsyncStateMachine die von IAsyncStateMachine ableitet). Damit man einen Einblick bekommt was abläuft, kann man den Code abbilden mit Task Continuations.
Das folgende Beispiel kommt von: https://weblogs.asp.net/dixin/understanding-c-sharp-async-await-1-compilation
In dieser Serie wird folgendes Beispiel als Grundlage genommen:
internal static async Task<int> MultiCallMethodAsync(int arg0, int arg1, int arg2, int arg3) { HelperMethods.Before(); int resultOfAwait1 = await MethodAsync(arg0, arg1); HelperMethods.Continuation1(resultOfAwait1); int resultOfAwait2 = await MethodAsync(arg2, arg3); HelperMethods.Continuation2(resultOfAwait2); int resultToReturn = resultOfAwait1 + resultOfAwait2; return resultToReturn; }
Die daraus resultierende Zustandsmaschine wird mit Task Continuations vereinfacht dargestellt.
internal static Task<int> MultiCallMethodAsync(int arg0, int arg1, int arg2, int arg3) { TaskCompletionSource<int> taskCompletionSource = new TaskCompletionSource<int>(); try { HelperMethods.Before(); MethodAsync(arg0, arg1).ContinueWith(await1 => { try { int resultOfAwait1 = await1.Result; HelperMethods.Continuation1(resultOfAwait1); MethodAsync(arg2, arg3).ContinueWith(await2 => { try { int resultOfAwait2 = await2.Result; HelperMethods.Continuation2(resultOfAwait2); int resultToReturn = resultOfAwait1 + resultOfAwait2; taskCompletionSource.SetResult(resultToReturn); } catch (Exception exception) { taskCompletionSource.SetException(exception); } }); } catch (Exception exception) { taskCompletionSource.SetException(exception); } }); } catch (Exception exception) { taskCompletionSource.SetException(exception); } return taskCompletionSource.Task; } internal class CompiledAsyncMethods { [DebuggerStepThrough] [AsyncStateMachine(typeof(MethodAsyncStateMachine))] // async internal static /*async*/ Task<int> MethodAsync(int arg0, int arg1) { MethodAsyncStateMachine methodAsyncStateMachine = new MethodAsyncStateMachine() { Arg0 = arg0, Arg1 = arg1, Builder = AsyncTaskMethodBuilder<int>.Create(), State = -1 }; methodAsyncStateMachine.Builder.Start(ref methodAsyncStateMachine); return methodAsyncStateMachine.Builder.Task; } }
Zum Glück muss man diese Details nicht verstehen um Erfolgreich async\await einzusetzen.
Wenn ein await verwendet wird, muss der Zustand der Methode gespeichert werden. Dazu wird eine Menge Daten gespeichert (hauptsächlich auf dem heap):
Auch diese Information ist nur da um ‘zur Kenntnis’ genommen zu werden.
Es gibt einige Orte wo await nicht verwendet werden kann:
Im nächsten Blog werden diverse Themen besprochen die mit promise Tasks zu tun haben. Es wird kurz das Unit Testen betrachtet, das Warten, Progress, Cancellation und Exceptions sind weitere Themen die erklärt werden.
[…] Das spart den Overhead vom Speichern und Zurücksetzen des Synchronization Contextes. Siehe Teil 11 der Serie C# Concurrency für […]