en / de
Expertisen
Methoden
Dienstleistungen
Referenzen
Jobs & Karriere
Firma
Technologie-Trends TechCast WebCast TechBlog News Events Academy

C# Concurrency Teil 11: Promise Tasks, async\await

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.

Was läuft genau mit async\await ab?

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.

Ablauf aus Benutzersicht

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.

C# Concurrency Blog: Ablauf async\await aus Benutzersicht

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.

So läuft’s ab

Bis am Aufruf Task.Delay:

  1. Wenn der Benutzer auf Start Klickt wird die Methode BtnStartAsyncAwaitSimple_Click() auf dem UI Thread ausgeführt. Diese Methode ruft StartAsyncAwaitSimpleAsync() auf.
  2. Der Aufruf Task.Delay() startet einen System Timer (nicht auf einen Threadpool Thread) und installiert einen Callback auf dem Timer. Das alles passiert immer noch auf dem UI Thread.
  3. Jetzt ist die Methode StartAsyncAwaitSimpleAsync() ‚pausiert‘ und die Kontrolle wird zurück gegeben an BtnStartAsyncAwaitSimple_Click().
  4. Auch BtnStartAsyncAwaitSimple_Click() wird ‚pausiert‘ und der UI Thread wird freigegeben.

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:

  1. Der Event vom System Timer ruft den Callback auf vom inneren await. Das passiert auf einem Threadpool Thread
  2. In diesem Fall (kein ConfigureAwait(false)) gibt es einen SynchronisationsContext (WPF) und der Aufruf wird remarshalled (über die Windows Message Queue) auf dem UI Thread. Der Rest der Methode StartAsyncAwaitSimpleAsync() (das Statement return 20), wird auf dem UI Thread ausgeführt.
  3. Die Kontrolle wird übergeben am BtnStartAsyncAwaitSimple_Click(). Auch dieser führt den restlichen Code aus auf dem UI-Thread.
  4. Wenn BtnStartAsyncAwaitSimple_Click() fertig ist, ist auch der Zyklus fertig.

Ablauf aus Threading-Sicht

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.

C# Concurrency Blog: async\await aus Threading Sicht

C# Concurrency Blog: async\await Ablauf aus Threading Sicht

So läuft’s ab

Bis am Aufruf Task.Delay:

  1. Wenn der Benutzer auf Start Klickt wird eine Message in die Windows Message Queue gepostet
  2. Der UI Thread verarbeitet die Message und ruft die Methode BtnStartAsyncAwaitSimple_Click() auf.
  3. Das await merkt sich sämtliche Zustände und den Return Point um später wieder die Methode weiter ausführen zu können. Weil ConfigureAwait(false) hinter dem await Aufruf steht, muss das await sich nicht den Synchronisation Context merken.
  4. Das await ruft StartAsyncAwaitSimpleAsync() auf.
  5. Das await merkt sich auch hier sämtliche Zustände und den Return Point um später wieder die Methode weiter ausführen zu können. Weil ConfigureAwait(false) hinter dem await Aufruf steht, muss das await sich nicht den Synchronisation Context merken.
  6. Der Aufruf Task.Delay() startet einen System Timer (nicht auf einem Threadpool Thread) und installiert einen Callback auf dem Timer. Das alles passiert immer noch auf dem UI Thread.
  7. Task.Delay() gibt einen promise Task zurück, welcher Daten vom asynchronen Aufruf beinhaltet.
  8. Jetzt wird die Methode StartAsyncAwaitSimpleAsync() ‚pausiert‘ und die Kontrolle wird zurück gegeben an BtnStartAsyncAwaitSimple_Click()
  9. Auch StartAsyncAwaitSimpleAsync() gibt eine promise Task zurück: Task<int> welcher Daten vom asynchronen Aufruf beinhaltet. Beim Ablauf auch den Wert, Status und eventuelle Exceptions.
  10. Auch BtnStartAsyncAwaitSimple_Click() wird ‚pausiert‘ und der UI Thread wird freigegeben.

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:

  1. Der Event vom System Timer ruft den Callback auf vom inneren await. Das passiert auf einem Threadpool Thread.
  2. Weil ConfigureAwait(false) ist, wird der Rest der Methode StartAsyncAwaitSimpleAsync(), (das Statment return 20) ausgeführt auf dem Thread Pool Thread.
  3. Die Kontrolle wird übergeben am BtnStartAsyncAwaitSimple_Click(). Das await packt den Rückgabe Wert aus und kontrolliert, ob es AggregateExceptions gibt. Auch diese Methode hat ConfigureAwait(false) und führt den restlichen Code auf dem Threadpool Thread aus.
  4. Wenn BtnStartAsyncAwaitSimple_Click() fertig ist, ist auch der Zyklus fertig.

 

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.

Das gleiche Beispiel mit UI Thread remarshalling

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.

C# Concurrency Blog: async\await Ablauf aus Threading Sicht

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

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.

Das Speichern vom Zustand der Methode

Wenn ein await verwendet wird, muss der Zustand der Methode gespeichert werden. Dazu wird eine Menge Daten gespeichert (hauptsächlich auf dem heap):

  1. Die Werte von allen lokalen Variablen:
    1. Parameter
    2. Variablen im scope
    3. Andere Variablen wie Schleifenzähler
    4. Die ‘this’ Variable (wenn die Methode nicht statisch ist)
  2. Punkt der Wiederaufnahme
  3. Stack
  4. Das Task-Objekt das zurückgegeben wird
  5. Kontexte:
    1. Synchronization context
    2. ExecutionContext
    3. SecurityContext
    4. CallContext

Auch diese Information ist nur da um ‘zur Kenntnis’ genommen zu werden.

Wo await nicht verwendet werden kann

Es gibt einige Orte wo await nicht verwendet werden kann:

  1. In catch und finally Blöcke
  2. In lock Sektionen
  3. In LINQ Query Expressions
  4. In unsafe Code

Follow up

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.

← Vorige Post
Kommentare

Eine Antwort zu “C# Concurrency Teil 11: Promise Tasks, async\await”

  1. […] Das spart den Overhead vom Speichern und Zurücksetzen des Synchronization Contextes. Siehe Teil 11 der Serie C# Concurrency für […]

Schreiben Sie einen Kommentar

Ihre E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Newsletter - aktuelle Angebote, exklusive Tipps und spannende Neuigkeiten

 Jetzt anmelden
NACH OBEN
Zur Webcast Übersicht