Wie ruft man eine asynchrone Methode aus einem synchronen Kontext in C# auf? Fleisch, kein Wasser
Stell dir vor: Du sitzt abends am Code und feilst an einem alten Konsolenprogramm in C#. Alles läuft synchron, wie ein Uhrwerk. Und dann – peng! – musst du eine API ansprechen, die nur über async Task realisiert ist. Sollst du etwa das ganze Programm umschreiben? Ein bekanntes Problem, oder?
Die Frage „How to call asynchronous method from synchronous method in C#“ ist eine der beliebtesten auf Stack Overflow. Und das zu Recht. Dort, in den Kommentaren, gibt es tonnenweise Ratschläge von „Mach einfach .Result und mach dir keine Sorgen“ bis „Bist du verrückt, du wirst einen Deadlock bekommen!“. Lass uns das Ganze systematisch aufschlüsseln: Welche Ansätze funktionieren wirklich und welche führen in die Hölle?
Warum funktioniert einfaches „await“ nicht?
Wenn du versuchst, Folgendes zu schreiben:
public void SyncMethod(){ await DoSomethingAsync(); // Fehler CS4032}Der Compiler wird sich einfach weigern, dies zu kompilieren. await kann nur innerhalb einer Methode verwendet werden, die mit dem Modifikator async gekennzeichnet ist. Und async void ist ein eigenes Kapitel, das wir auch noch behandeln werden.
Also, wir haben eine asynchrone Methode:
public async Task<string> GetDataAsync(){ using var httpClient = new HttpClient(); return await httpClient.GetStringAsync("https://api.example.com/data");}Und es gibt eine synchrone Methode, von der aus wir sie aufrufen wollen. Los geht's mit den Varianten.
Ansatz 1: GetAwaiter().GetResult() – Hacker-Chic
Der beliebteste und wohl eleganteste Weg:
public string CallSync(){ return GetDataAsync().GetAwaiter().GetResult();}Warum das cool ist:
- Es generiert keine
AggregateException(im Gegensatz zu.Result). Wenn innerhalb der asynchronen Methode ein Fehler auftritt, erhältst du den ursprünglichen Typ, nicht einen Wrapper. - Der Code liest sich gut, ohne unnötige try-catch-Blöcke.
Aber es gibt einen Haken: Wenn deine synchrone Methode in einem Kontext mit SynchronizationContext ausgeführt wird (z. B. im UI-Thread von WinForms oder WPF), riskierst du einen Deadlock – eine gegenseitige Blockade von Threads. Thread A wartet auf die Ausführung der asynchronen Aufgabe, und die Aufgabe kann nicht abgeschlossen werden, weil sie darauf wartet, dass Thread A freigegeben wird. Beschissen.
Wie man es vermeidet: Verwende .ConfigureAwait(false) in der asynchronen Methode selbst. Aber das ändert bereits die API, nicht nur den aufrufenden Code.
Ansatz 2: Task.Run(async () => await ...).GetAwaiter() – Auslagerung in den Thread-Pool
Wenn du die asynchrone Methode nicht ändern kannst, aber einen Deadlock garantiert vermeiden willst – lagere die Aufgabe in den Thread-Pool aus:
public string CallSyncSafe(){ return Task.Run(async () => await GetDataAsync()).GetAwaiter().GetResult();}Magie: Task.Run startet das Delegat auf einem Thread aus dem ThreadPool, der keinen SynchronizationContext hat. Ein Deadlock ist unmöglich. Selbst wenn die ursprüngliche Methode ConfigureAwait(true) verwendet.
Nachteil: Ein zusätzlicher Kontextwechsel (context switch). Für hunderte Aufrufe nicht optimal. Für einmalige Aufrufe ideal.
Ansatz 3: .Result oder .Wait() – Omas Methode
public string CallSyncOldSchool(){ return GetDataAsync().Result; // Oder .Wait()}Was daran schlecht ist:
- Bei einem Fehler wird eine
AggregateExceptionausgelöst. Um zum eigentlichen Fehler zu gelangen, muss man inInnerExceptiongraben. - Das gleiche Deadlock-Risiko im UI-Kontext.
- Der Code sieht aus wie ein Workaround aus dem Jahr 2012.
Dieser Ansatz ist ein rotes Tuch für Code-Reviews. Wenn ich so etwas in einem Pull-Request sehe – lehne ich ihn ab.
Ansatz 4: AsyncHelper oder benutzerdefiniertes RunSync
Erfahrene Entwickler schreiben manchmal ihre eigenen Helfer. Zum Beispiel so einen: