CodeGym /Kurse /C# SELF /Zusammenspiel von asynchronem und synchronem Code

Zusammenspiel von asynchronem und synchronem Code

C# SELF
Level 62 , Lektion 4
Verfügbar

1. Einführung

Asynchronität in C# ist ein mächtiges Werkzeug. Aber manchmal stehst du vor der Situation, dass asynchroner Code aus synchronem aufgerufen werden muss (oder umgekehrt). Auf dem Papier sollte das "magisch" funktionieren, in der Praxis können seltsame Hänger (deadlock), Performance-Verluste und sogar ein zerstörtes User-Interface auftreten. Oft zeigt sich der Bug erst mit echten Daten: in Produktion oder beim Nutzer. Die Ursache liegt meist im falschen Zusammenspiel von synchronem und asynchronem Code und in nicht offensichtlichen Details des .NET-Task-Schedulers.

Außerdem benutzen moderne Libraries und Frameworks stark asynchrone Methoden, und du musst wissen, wie du asynchronen Code sauber in bestehende synchrone Call-Strecken "einklebst" oder wie du korrekt synchronen Code aus einem async-Method aufrufst.

Kurze Wiederholung: was passiert bei await

Wenn du schreibst:

await SomeAsyncMethod();

Der Code wird in zwei Teile "geschnitten": vor dem await und nach dem. Der erste Teil läuft bis zur ersten erwarteten asynchronen Aktion (z.B. bis zu einem Netzwerkrequest), die Fortsetzung läuft danach. Die Frage: wo wird die Fortsetzung ausgeführt? Auf dem gleichen Thread? Auf einem anderen? Bei Desktop-Apps (z.B. WPF oder WinForms) vs. Konsole? Die Antwort: es hängt davon ab. Hier kommt z.B. ConfigureAwait ins Spiel.

Was ist SynchronizationContext?

SynchronizationContext ist ein spezieller Mechanismus in .NET, mit dem Code "merken" kann, wo und wie die Fortsetzung einer asynchronen Operation aufgerufen werden soll.

  • In klassischen WinForms/WPF-Anwendungen sorgt SynchronizationContext dafür, dass nach dem await der verbleibende Teil der Methode auf demselben Thread wie das UI weiterläuft, damit es keine "Zugriff vom falschen Thread"-Fehler gibt.
  • In ASP.NET (älteres, nicht Core) erlaubt SynchronizationContext das Wiederherstellen von HttpContext und das Fortsetzen der Arbeit mit der HTTP-Anfrage.
  • In Konsolen- und ASP.NET Core-Anwendungen fehlt der SynchronizationContext in der Regel (er ist null), und Fortsetzungen laufen auf dem Threadpool.

Was ist TaskScheduler?

TaskScheduler ist ein niedriger liegender Mechanismus. In den meisten Fällen arbeitest du mit TaskScheduler.Default, der den .NET-Threadpool nutzt. Er bestimmt, welche Tasks wann und wo ausgeführt werden.

2. Deadlock beim Mischen von await und Result/Wait()

Einer der bekanntesten Fallen in C#:

// Irgendwo im UI-Code
var result = SomeAsyncMethod().Result;

oder

SomeAsyncMethod().Wait();

Und plötzlich: die Anwendung hängt. Warum?

Wie kommt das zustande?

  1. Du rufst eine asynchrone Methode auf und fragst sofort .Result oder .Wait() ab — du blockierst also den aktuellen Thread und wartest auf das Ende der asynchronen Task.
  2. SomeAsyncMethod macht intern ein await und plant die Fortsetzung so, dass sie auf demselben Thread über SynchronizationContext ausgeführt wird — aber dieser Thread ist bereits durch das Warten auf Result/.Wait() blockiert.
  3. Da der Thread wartet, kann er die Fortsetzung (continuation) nicht ausführen.
  4. Fertig: Deadlock — der Thread wartet auf sich selbst.

Das ist besonders leicht in UI-Apps zu reproduzieren, wo alles auf dem UI-Thread läuft und Fortsetzungen genau diesen Thread brauchen. In Konsolenanwendungen und in ASP.NET Core (ohne SynchronizationContext) passieren solche Deadlocks selten.

Anekdote aus dem Leben: Wenn es dir gelungen ist, einen Deadlock mit .Result zu erzeugen — Glückwunsch, du bist ein paar Schritte näher am Senior-Level :D

3. Wie bindet man asynchronen Code korrekt in synchronen ein?

Empfehlung Nr. 1: Asynchronität von oben nach unten

Wenn du eine asynchrone Methode einführst, heb async/await die Call-Chain nach oben bis zum UI oder Entry-Point. Versuche nicht, Asynchronität nur halbherzig zu halten.

Schlecht (blockiert den Thread):

// Synchrone Methode ruft asynchrone via .Result auf
public void DoStuff()
{
    var data = GetDataAsync().Result;
    // ...
}

Gut (Asynchronität durchgezogen):

public async Task DoStuffAsync()
{
    var data = await GetDataAsync();
    // ...
}

Wenn möglich — benutze immer await statt .Result / .Wait().

4. Aber es gibt Situationen: async aus sync aufrufen

Das obere Tree auf async umbauen (beste Variante, wenn du die Wahl hast).

Spezielle Patterns verwenden: z.B. die Task auf einem separaten Thread via Task.Run starten und darin die async-Methode aufrufen.

public void DoStuff()
{
    var result = Task.Run(() => SomeAsyncMethod()).Result;
}

Aber: auch hier gibt es Feinheiten mit Synchronisation in UI-Apps, also mach das nicht ohne triftigen Grund.

5. Was macht ConfigureAwait(false)?

Manchmal brauchst du nicht, dass nach dem await der Code auf demselben Thread weiterläuft (z.B. auf Servern oder in Libraries, wo SynchronizationContext keine Rolle spielt). Im Gegenteil: besser ist es, wenn .NET nicht an einen bestimmten Thread gebunden bleibt — das beschleunigt die Ausführung.

Sytax und Prinzip

await SomeAsyncMethod().ConfigureAwait(false);
  • ConfigureAwait(false) sagt: "Ich brauche den originalen SynchronizationContext nicht, setz die Fortsetzung irgendwo fort, auch auf einem anderen Thread".
  • ConfigureAwait(true) (Standard) — "Fahre fort dort, wo das await aufgerufen wurde, vorzugsweise im selben SynchronizationContext".

Visuelles Schema


        ┌─────────────────────────────────────────────────────┐
        │                     SynchronizationContext          │
        └─────────────────────────────────────────────────────┘
                      ↑                             ↑
       (UI-Thread)   await SomeAsyncMethod()        Continuation (nach await)
                  ──────────────────────────────>  (derselbe Thread — wenn ConfigureAwait(true))
                      ↓
                    (beliebiger Thread — wenn ConfigureAwait(false))

Beispiel für ConfigureAwait(false) — "Library"-Code

Stell dir vor, du schreibst eine Bibliothek, die von WinForms, WPF, ASP.NET, Konsolenapps usw. benutzt werden kann…

Du solltest dich nicht von deren Thread-Modell abhängig machen. Darum verwende in async-Methoden deiner Library ConfigureAwait(false):

public async Task<string> LoadDataFromUrlAsync(string url)
{
    using var client = new HttpClient();
    string content = await client.GetStringAsync(url).ConfigureAwait(false);
    return content;
}

Jetzt verlangt deine Methode nicht, auf einem bestimmten SynchronizationContext ausgeführt zu werden. Das ist sicherer und performanter (weniger Thread-Switches).

Beispiel: was passiert mit await ohne ConfigureAwait

Betrachten wir eine WPF-App:

private async void Button_Click(object sender, RoutedEventArgs e)
{
    Button1.Content = "Wird geladen...";
    await Task.Delay(2000);  // Simulation einer langen Operation
    Button1.Content = "Fertig!";
}

Task.Delay macht intern ein "await". Standardmäßig kommt die Steuerung nach dem await zurück auf den UI-Thread, damit du weiterhin UI-Elemente aktualisieren kannst.

Wenn du innerhalb der langen Operation ConfigureAwait(false) benutzt:

await Task.Delay(2000).ConfigureAwait(false);
Button1.Content = "Fertig!";  // Fehler!

Es wird eine Ausnahme geben: InvalidOperationException: "The calling thread cannot access this object because a different thread owns it."
Weil die Fortsetzung jetzt auf einem fremden Thread läuft und du nicht auf das UI zugreifen darfst.

Fazit: Verwende ConfigureAwait(false) nur dort, wo kein UI-/Kontextzugriff nötig ist.

6. Nützliche Feinheiten

Wo und wann ConfigureAwait verwenden

Szenario Sollte .ConfigureAwait(false) verwendet werden? Warum?
Bibliothekscode Ja Wird in beliebigem Kontext aufgerufen, UI nicht nötig
Innerhalb von ASP.NET Core-Code Ja (Kontext ist meist nicht vorhanden) Steigert die Performance
In WinForms/WPF, bei Zugriff auf UI Nein Du musst auf den UI-Thread zurückkehren
Synchrone Methoden Hat keinen Sinn Kein SynchronizationContext vorhanden
In Konsolenanwendungen Kann, aber kein sichtbarer Effekt Kein Kontext

Wie erkennst du, ob ein SynchronizationContext existiert?

Kannst du direkt im Code prüfen:

Console.WriteLine(SynchronizationContext.Current == null
    ? "Kein Kontext"
    : "Kontext existiert");
  • In Konsolen- und ASP.NET Core-Apps wird "Kein Kontext" ausgegeben.
  • In WinForms/WPF — "Kontext existiert".

Visualisierung der Thread-Wechsel

sequenceDiagram
    participant MainThread as Haupt-Thread (UI/Console)
    participant ThreadPool as Thread aus dem Pool

    MainThread->>SomeAsyncMethod: Aufruf der Methode
    SomeAsyncMethod->>MainThread: await ohne ConfigureAwait
    Note right of MainThread: Nach await: zurück auf denselben Thread
    SomeAsyncMethod->>ThreadPool: await mit ConfigureAwait(false)
    Note right of ThreadPool: Nach await kann auf beliebigem Thread weitergearbeitet werden

Kurze Regeln

  • Verwende .Result und .Wait() nicht in UI-Code mit asynchronen Methoden.
  • In Bibliothekscode benutze konsequent ConfigureAwait(false) für alle awaits.
  • In UI-Apps verwende ConfigureAwait(false) nur innerhalb von Methoden, die nicht auf UI-Elemente zugreifen.
  • Versuche, synchronen und asynchronen Code nicht zu mischen, wenn es nicht nötig ist.
  • Wenn du async aus sync aufrufen musst — überleg zweimal: Kannst du nicht die ganze Kette async machen?

7. Typische Fehler von Einsteigern beim Arbeiten mit Asynchronität

Fehler Nr.1: überall .Result oder .Wait() nutzen.
Sehr oft fangen Entwickler, nach ein paar Artikeln über Asynchronität, an, .Result oder .Wait() überall einzubauen, um asynchrone Aufrufe zu "synchronisieren". Auf den ersten Blick praktisch, in Wahrheit ein garantierter Weg zum Deadlock in UI-Anwendungen. Besonders gefährlich, wenn in asynchronen Methoden kein ConfigureAwait(false) benutzt wird — der UI-Thread blockiert und kann die Fortsetzung nicht ausführen.

Fehler Nr.2: falsche oder zu großzügige Verwendung von ConfigureAwait(false).
Manche Anfänger setzen ConfigureAwait(false) überall ein, auch in Code, der UI-Elemente anfasst. In UI-Apps führt das zu Zugriffsfehlern auf UI-Elemente (InvalidOperationException), weil die Fortsetzung nun auf einem anderen Thread läuft.

Fehler Nr.3: vergessen, dass ConfigureAwait nur mit awaitable-Objekten funktioniert.
Viele denken, man könne ConfigureAwait(false) für beliebige Methoden nutzen, aber es wirkt nur auf Objekte, die Task, Task<T>, ValueTask oder andere awaitable-Typen zurückgeben. Für synchrone Methoden hat ConfigureAwait keinen Effekt — Sync bleibt Sync.

Fehler Nr.4: unnötiges Mischen von sync und async.
Versuche, async-Methoden aus Sync-Code ohne Strategie aufzurufen (z.B. mit .Result, .Wait() oder Task.Run) — das führt oft zu subtilen Bugs, Performance-Problemen und schwer zu diagnostizierenden Deadlocks. Auch wenn eine Methode kurz aussieht, können solche Aufrufe die ganze Anwendung destabilisieren.

Fehler Nr.5: Unterschätzung des Einflusses von SynchronizationContext.
Anfänger vergessen oft, dass die Fortsetzung nach einem await auf demselben Thread (UI) laufen kann, wenn man ConfigureAwait(false) nicht verwendet. Das führt zu unvorhersehbarem Verhalten, besonders wenn UI- und Bibliothekscode kombiniert werden und Fortsetzungen/Waits sich überlappen.

1
Umfrage/Quiz
Asynchrone Datenströme, Level 62, Lektion 4
Nicht verfügbar
Asynchrone Datenströme
Tiefer Einblick in Asynchronität
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION