Was ist ConfigureAwait und warum sollte „False“ der Standard sein?

ConfigureAwait für besseres Handling von Tasks – ein Standard für erfahrene .NET-Entwickler

ConfigureAwait(False) als Standard für erfahrene Entwickler

Erfahre im heutigen Beitrag, warum „ConfigureAwait“ der Standard für fortgeschrittene Entwickler sein sollte. Hierzu schauen wir uns natürlich auch Beispiele ohne die benannte – ich nenne Sie mal – „Konfigurations“-Methode an. Insbesondere fällt hier auch ins Gewicht, welche Hürden man als Anfänger dabei bewältigen muss. Schaue Dir hierzu auch gerne die Beispiele in VB.NET und C# an!

Was ist, bzw. wofür ist ConfigureAwait?

Ganz plump übersetzt und zusammengefasst: Die „ConfigureAwait“-Methode konfiguriert eine Task so, dass dessen Folgeanweisungen auf dem Thread des Synchronisations-Kontextes ausgeführt (oder nicht ausgeführt) werden. Für gewöhnlich steht diese Konfiguration auf „True“, daher „springt“ die Ausführung nach einer erwarteten Task immer wieder auf den Thread des Synchronisations-Kontextes zurück.

Als Video auf YouTube ansehen

Hast Du keine Lust auf Text? Dann besuche einfach mein passendes Video auf YouTube dazu. Ich habe mir größte Mühe gegeben, es einfach und verständlich zu erklären.

Wieso sollte ich nach Möglichkeit ConfigureAwait(False) verwenden?

Man sollte ConfigureAwait(False) nach Möglichkeit verwenden, damit man eventuellem, permanentem ContextSwitching aus dem Weg geht. Das sähe dann vereinfacht gesagt so aus: Hintergrund, Vordergrund, Hintergrund, Vordergrund,… Es wäre hingegen doch wesentlich schöner, wenn wir erst alles im Hintergrund erledigen und dann final im Vordergrund bleiben, oder?

Nun hast Du bereits erfahren, dass bei entsprechender Konfiguration (standardmäßig True) ein eventuelles, ständiges Hopping zwischen den Threads stattfindet. Stell Dir vor, Du würdest eine Web-Schnittstelle aufrufen, oder eine Datei einlesen. Dies machst Du für gewöhnlich am besten asynchron, damit die grafische Oberfläche dabei nicht hängt. Stell Dir nun 5 Aufgaben vor, Welche dieses „in den Hintergrund auslagern“ verwenden.

Es wäre doch blöd, wenn man nach jeder ausgeführten Aufgabe erst wieder zurückkommt, nur um dann wieder unnötig hin und her zu hüpfen.

Das ist ungefähr so, als würde ich Dir abwechselnd sagen „Geh‘ weg, komm‘ zurück, geh‘ weg…“. Spätestens nach dem zweiten Mal würdest Du mir vermutlich einen Vogel zeigen. Dies solltest Du bei der Konzeption / der Architektur Deiner Anwendungen auch im Kopf behalten. Leider kann der Computer uns hier nicht ebenfalls einen Vogel zeigen, denn Der macht letztendlich nur das, was Du Ihm durch Code sagst – KI und Co. jetzt mal weggelassen 😉.

Wie sieht der Code ohne ConfigureAwait-Konfiguration aus?

Damit wir die Vorgehensweise und den positiven Effekt hinter „ConfigureAwait“ noch besser verstehen können, schauen wir es uns Schritt für Schritt an. Im ersten Schritt, bauen wir einfach mal ein wenig Code, ohne großartig darüber nachzudenken (wie leider viele Anfänger/Junior-Developer). Bei diesem Code ist dies auch nicht weiter „problematisch“ – bis auf das ggf. fehlende Wissen dahinter.

Wirf also nun einen Blick auf folgenden, simplen Code, Welcher die statische „Delay“-Methode der Task-Klasse verwende. Packe einen Knopf und ein Label auf Deine Form und generiere den Klick-Handler für den Button. Da wir uns hier nur auf das Wesentliche konzentrieren gehe ich an dieser Stelle davon aus, dass Du Dich mit dem erstellen eines Button-Klick-Handlers auskennst.

Ein wenig mit Hilfe der Task warten

Hierbei ist es auch relativ egal, ob Du nun Winforms oder WPF verwendest. Achte nur darauf, dass Du die Methode mit „Async“ (VB.NET) / „async“ (C#) markierst. Schreibe nun folgenden Code hinein und schaue, wie sich die Anwendung bei einem Klick verhält:

Private Async Sub Button1_Click(sender As Object, e As EventArgs)
  ' vor dem Delay (der "erwarteten" Task)
  Await Task.Delay(1000)
  ' nach dem Delay (nach der "erwarteten" Task)
  Label1.Text = "Mein neuer Text!"
End Sub
private async void Button1_Click(object sender, EventArgs e)
{
  ' vor dem Delay (der "erwarteten" Task)
  await Task.Delay(1000)
  ' nach dem Delay (nach der "erwarteten" Task)
  Label1.Text = "Mein neuer Text!";
}

Erstmal alles in Ordnung, oder? Die Anwendung startet und wartet darauf, dass Du etwas machst. Wenn Du auf den Button klickst, wartet „die Anwendung“ eine Sekunde und der Text wird erfolgreich gesetzt. Hier ist wichtig zu verstehen, dass die Anwendung tatsächlich im Hintergrund wartet, ansonsten wäre Deine grafische Oberfläche währenddessen eingefroren.

Wie das Ganze non-async aussieht

Hierzu kannst Du auch einmal die „Thread.Sleep“-Variante nehmen, also ohne async und await, dann weißt Du was ich meine 🤓! Du startest die Anwendung wie vorher, klickst auf den Button und merkst dann ups, ich kann das Fenster weder bewegen, noch vergrößern/verkleinern und auch der Text ändert sich nicht? Die Wartezeit habe ich bewusst hochgestellt – damit es noch klarer sichtbar ist, denn z. B. das Label ändert sich erst nachdem die Zeit abgelaufen ist.

Private Sub Button1_Click(sender As Object, e As EventArgs)
  ' vor dem Sleep
  System.Threading.Thread.Sleep(5000)
  ' nach dem Sleep
  Label1.Text = "Mein neuer Text!"
End Sub
private void Button1_Click(object sender, EventArgs e)
{
  ' vor dem Sleep
  System.Threading.Thread.Sleep(5000)
  ' nach dem Sleep
  Label1.Text = "Mein neuer Text!";
}

Nun hast Du gesehen und ggf. verstanden, was es zumindest für Auswirkungen hat, wenn der Thread der grafischen Oberfläche „zu“ beschäftigt ist.

Ein Beispiel mit ConfigureAwait(False)

Ich kann Dich schon grummeln hören: „Ja wenn das so wichtig ist, wie sieht denn dann ein konfiguriertes Beispiel aus? Wenn True der Standardwert ist, wie sieht es dann beim – Deiner Meinung nach – richtigen False aus?“. Das ist eine gute Frage und Dieser widmen wir uns jetzt – schaue Dir den vorherigen Code nochmal, aber nun wie folgt an. Bedenke dabei, dass der Wert „True“ standardmäßig voreingestellt ist, wir aber nun explizit „False“ angeben:

Private Async Sub Button1_Click(sender As Object, e As EventArgs)
  ' vor dem Delay (der "erwarteten" Task)
  Await Task.Delay(1000) _ ' wegen der Leserlichkeit umgebrochen
    ConfigureAwait(False)
  ' nach dem Delay (nach der "erwarteten" Task)
  Label1.Text = "Mein neuer Text!"
End Sub
private async void Button1_Click(object sender, EventArgs e)
{
  // vor dem Delay (der "erwarteten" Task)
  await Task.Delay(1000) // wegen der Leserlichkeit umgebrochen
    .ConfigureAwait(false);
  // nach dem Delay (nach der "erwarteten" Task)
  Label1.Text = "Mein neuer Text!";
}

Huch, jetzt gibt es einen Fehler?

Nachdem Du den obigen Code ausgeführt hast, fragst Du Dich nun bestimmt: „Toll, jetzt habe ich’s genauso gemacht, wie Du sagtest, aber jetzt gibt es einen Fehler, warum? Wie soll mir das helfen?“. Ich kann Deine Aufregung als vermutlicher Anfänger (ggf. sogar Fortgeschrittener) erstmal verstehen. Ich erkläre Dir erstmal was passiert ist und warum wir diesen Fehler bekommen.

Wie oben bereits erwähnt, steht „ConfigureAwait“ praktisch standardmäßig auf „True“, also wenn wir es gar nicht angeben. Dies bewirkt, dass wir uns so gesehen vor der Ausführung der „Await“-beinhaltenden Zeile (im obigen Beispiel) auf dem Thread der grafischen Oberfläche befinden. Dies ist der „captured context“, also der abgefangene/gefangene Thread a la „Ah okay, also auf diesem Thread soll ich laufen?“.

Solange wir Änderungen an der grafischen Oberfläche vornehmen wollen, ist dies auch super, aber warum der Fehler kommt, hörst Du nun. Wenn wir nach dem „Await“ durch „ConfigureAwait(False)“ eben NICHT mehr auf den „captured context“ zurückkommen, dann können wir darin auch nicht arbeiten. Wir können die grafische Oberfläche also nun nicht mehr ändern. Wann und warum das sinnvoll sein kann, bzw. wie das genau aussieht, schauen wir uns im nächsten Abschnitt an.

Wann ist „False“ für ConfigureAwait sinnvoll?

Nachdem unser letzter Code-Umbau für Dich vermutlich eher etwas Negatives erzeugt hat, bringen wir nun Licht ins Dunkel, wann die Verwendung von „False“ sinnvoll ist. Eigentlich könnte man sagen, dass die Verwendung von „False“ als Default-Wert, seine starke Daseinsberechtigung hat, allerdings in manchen Fällen wie oben zu Problemen führt.

Schaue Dir für das nähere Verständnis einmal den folgenden Service isoliert an. Dieser macht Gebrauch von einer kostenlosen Web-Schnittstelle, Welche uns eine zufällige Aktivität für den Alltag vorschlägt – einen simplen String.

Imports System.Net.Http
Imports System.Text.Json

Public Class ActivityService

    Private _httpClient As HttpClient

    Private Const API_URL As String = "https://www.boredapi.com/api/activity"

    Sub New(httpClient As HttpClient)
        _httpClient = httpClient
    End Sub

    Public Async Function GetRandomActivityAsync() As Task(Of String)
        Dim response = Await _httpClient _
            .GetAsync(API_URL) _
            .ConfigureAwait(False)
        Dim contentStream = Await response.Content _
            .ReadAsStreamAsync() _
            .ConfigureAwait(False)
        Dim options = New JsonSerializerOptions() With {
            .PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        }
        Dim jsonObject = Await JsonSerializer _
            .DeserializeAsync(Of ActivityResponse)(contentStream, options) _
            .ConfigureAwait(False)
        Return jsonObject.Activity
    End Function

End Class
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualBasic;
using System.Net.Http;
using System.Text.Json;

public class ActivityService
{
    private HttpClient _httpClient;

    private const string API_URL = "https://www.boredapi.com/api/activity";

    public ActivityService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<string> GetRandomActivityAsync()
    {
        var response = await _httpClient
           .GetAsync(API_URL)
           .ConfigureAwait(false);
        var contentStream = await response.Content
            .ReadAsStreamAsync()
            .ConfigureAwait(false);
        var options = new JsonSerializerOptions()
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };
        var jsonObject = await JsonSerializer
            .DeserializeAsync<ActivityResponse>(contentStream, options)
            .ConfigureAwait(false);
        return jsonObject.Activity;
    }
}

In der Funktion (Welche eine Task zurückgibt!) „GetRandomActivityAsync“ siehst Du einige Verwendungen mit dem übergebenen Parameter-Wert „False“. Diese bewirken, dass wir nicht nach jedem „Await“ wieder zurück in den ursprünglich „gefangenen“ Kontext hüpfen. Dies ist natürlich wesentlich sauberer und vor allem effizienter!

Der Aufruf aus der Form aus könnte daher nun wie gleich folgend aussehen. Wir verwenden unseren Service und lassen die Ausführung anschließend wieder durch „ConfigureAwait(True)“ – was ja der Standardwert ist – zurück in den alten Thread hüpfen. Dabei handelt es sich um den Thread, in Welchem wir die grafische Oberfläche verändern können, nice!

Nun haben wir also den Service durch die Verwendung von „ConfigureAwait(False)“ an den entsprechenden Stellen effizienter und sauberer gestaltet. Trotzdem können wir den Form-Code nach wie vor so benutzen, wie wir es gewohnt sind.

' irgendwo weiter oben in der Klasse
Private _activityService As ActivityService

Sub New(activityService As ActivityService)
  ' per Dependency Injection injiziert..
   _activityService = activityService
End Sub

Private Async Sub Button1_Click(sender As Object, e As EventArgs)
  ' vor dem Delay (der "erwarteten" Task)
  Dim response = Await _activityService.GetRandomActivityAsync()
  ' nach dem Delay (nach der "erwarteten" Task)
  Label1.Text = "Mein neuer Text!"
End Sub
    // irgendwo weiter oben in der Klasse
    private ActivityService _activityService;

    public ActivityService(ActivityService activityService)
    {
        // per Dependency Injection injiziert..
        _activityService = activityService;
    }

    private async void Button1_Click(object sender, EventArgs e)
    {
        // vor dem Delay (der "erwarteten" Task)
        var response = await _activityService.GetRandomActivityAsync();
        // nach dem Delay (nach der "erwarteten" Task)
        Label1.Text = "Mein neuer Text!";
    }

Fazit

Fazit: ConfigureAwait(False) als Standard für erfahrene Entwickler
Fazit: ConfigureAwait(False) als Standard für erfahrene Entwickler

Wer als .NET-Entwickler einen saubereren und effizienteren Programmierstil im Bezug auf Tasks und Co. an den Tag legen möchte, sollte unbedingt „ConfigureAwait(False)“ an passenden Stellen nutzen. Da man vermutlich wesentlich häufiger eben nicht in den alten Synchronisations-Kontext wechseln muss, empfiehlt sich daher eher das Paradigma „False-first“. Dies kann jedoch besonders bei Anfängern zu Verwirrung führen – aber wie heißt es so schön: „Learning by doing“, gell!?

Weiterführende Links

Schreibe einen Kommentar

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