Bei der Verwendung von async / await in WPF / WinForms ist das Verwirrendste zu welchem Thread nach dem await zurückgekehrt wird und wann die UI berührt werden darf.
Vor allem wenn Dispatcher, BeginInvoke, ConfigureAwait(false) und .Result / .Wait() sich vermischen, werden die Ursachen für eingefrorene Oberflächen und Cross-Thread-Ausnahmen schwer erkennbar.
Dieser Artikel behandelt ausschließlich die Beziehung zwischen dem UI-Thread von WPF / WinForms und async / await.
Für die allgemeinen Entscheidungskriterien zu async / await insgesamt siehe den verwandten Artikel C# async/await-Praxis-Entscheidungstabelle – Task.Run und ConfigureAwait.
Die Stellen, an denen es in der Praxis wirklich wehtut, sind ungefähr diese:
- Unklar, wo die Fortsetzung nach
awaitläuft - Unklar, ob nach einem
Task.Rundie UI berührt werden darf - Unsicherheit, wo
ConfigureAwait(false)gesetzt werden soll - Die Oberfläche friert bei
.Result/.Wait()/.GetAwaiter().GetResult()ein - WPFs
Dispatcherund WinForms’Invoke/BeginInvoke/InvokeAsyncvermischen sich im Kopf
WPF und WinForms sind beide auf den UI-Thread zentrierte Modelle.
Deshalb hilft es bei der Klärung von async / await weniger, sich philosophisch mit der Frage „Was ist eigentlich Asynchronität?“ zu beschäftigen, sondern klarzustellen, was gerade mit dem UI-Thread und der Nachrichtenschleife geschieht.
Dieser Artikel geht vor allem von WPF / WinForms-Anwendungen ab .NET 6 aus und behandelt in einer praxistauglichen Reihenfolge, wohin die Ausführung nach await zurückkehrt, den Dispatcher, ConfigureAwait(false) sowie die Gründe, warum .Result / .Wait() blockieren.
Zu beachten: Control.InvokeAsync in WinForms gibt es erst ab .NET 9.
In älteren WinForms-Versionen verwenden Sie grundsätzlich BeginInvoke / Invoke.
Der in diesem Artikel gezeigte Code ist außerdem als vollständiges, bau- und lauffähiges Beispielpaket (eine UI-unabhängige Bibliothek, WPF- / WinForms-Beispiele sowie Unittests, die den Rückkehrort von await und das Deadlock-Szenario nachbilden) auf GitHub veröffentlicht.
wpf-winforms-ui-thread-async-await-one-sheet - komurasoft-blog-samples (GitHub)
Inhaltsverzeichnis
- Zuerst das Fazit (in einem Satz)
- Erst einmal auf einen Blick
- 2.1. Das Gesamtbild
- 2.2. Die erste Entscheidungstabelle
- In diesem Artikel verwendete Begriffe
- 3.1. Der UI-Thread und die Nachrichtenschleife
- 3.2.
SynchronizationContext/Dispatcher/Invoke
- Typische Muster
- 4.1. Plain
awaitin einem UI-Ereignishandler - 4.2.
Task.Runnur für schwere CPU-Berechnungen - 4.3.
ConfigureAwait(false)bedeutet „keine erzwungene Rückkehr“, nicht „garantiert keine Rückkehr“ - 4.4. Warum
.Result/.Wait()/.GetAwaiter().GetResult()blockieren
- 4.1. Plain
- Wann
Dispatcher/Invokeverwenden - Häufige Antipatterns
- Checkliste für Reviews
- Grobe Entscheidungshilfe
- Zusammenfassung
- Referenzen
1. Zuerst das Fazit (in einem Satz)
- Bei einem plain
awaitin einem WPF- / WinForms-UI-Ereignishandler dürfen Sie davon ausgehen, dass die Fortsetzung nachawaitgrundsätzlich zum UI-Thread zurückkehrt Task.Rundient dazu, CPU-Berechnungen vom UI-Thread wegzuverlagern – nicht dazu, I/O-Wartezeiten einzupacken- Auch bei
await Task.Run(...)in einem UI-Handler kehrt die Fortsetzung normalerweise zum UI-Thread zurück, sofern diesesawaitein plainawaitist ConfigureAwait(false)bedeutet, dass diesesawaitdie Rückkehr zum eingefangenen UI-Kontext nicht erzwingt. Die UI in der anschließenden Fortsetzung direkt zu berühren ist gefährlich.Result/.Wait()/.GetAwaiter().GetResult()blockieren den UI-Thread. Muss die Fortsetzung vonawaitzur UI zurückkehren, blockiert es ziemlich zuverlässig- Für eine explizite Rückkehr zur UI in WPF:
Dispatcher.InvokeAsync - Für eine explizite Rückkehr zur UI in WinForms: klassisch
BeginInvoke, ab .NET 9 passtInvokeAsyncgut zum async-Ablauf - Die erste Richtlinie: die äußerste UI-Schicht bei plain
awaitbelassen, in allgemeinen BibliothekenConfigureAwait(false)erwägen und die Rückkehr zur UI nur dort explizit machen, wo es nötig ist
Kurz gesagt reicht es in WPF / WinForms, diese drei Dinge im Blick zu behalten:
- Auf welchem Thread gerade ausgeführt wird
- Wohin die Fortsetzung von
awaitzurückkehrt - Wer die Verantwortung für die Rückkehr zur UI trägt
Damit wird der Überblick deutlich klarer.
2. Erst einmal auf einen Blick
2.1. Das Gesamtbild
Am schnellsten erfassen Sie das Gesamtbild mit diesem Diagramm.
flowchart LR
A["UI-Ereignishandler<br/>(WPF / WinForms)"] --> B["plain await<br/>I/O-API"]
B --> C["Fängt den UI-SynchronizationContext ein"]
C --> D["Setzt sich nach await auf dem UI-Thread fort"]
D --> E["UI-Updates lassen sich direkt schreiben"]
A --> F["await Task.Run(...)<br/>schwere CPU-Verarbeitung"]
F --> G["Die eigentliche Berechnung läuft im ThreadPool"]
G --> H["Setzt sich nach await auf dem UI-Thread fort"]
H --> E
A --> I["await SomeAsync().ConfigureAwait(false)"]
I --> J["Erzwingt keine Rückkehr zur UI"]
J --> K["Fortsetzung auf beliebigem Thread"]
K --> L["Direktes UI-Update gefährlich<br/>Dispatcher / Invoke erforderlich"]
A --> M["SomeAsync().Result / Wait()<br/>GetAwaiter().GetResult()"]
M --> N["Blockiert den UI-Thread"]
N --> O["Fortsetzung kann nicht zur UI zurückkehren"]
O --> P["Hänger / Deadlock / mindestens ein Einfrieren"]
In der Praxis begegnen Ihnen im Wesentlichen diese vier Muster.
- Plain
awaitin einem UI-Ereignishandler Task.Runin einem UI-Ereignishandler, um CPU-Arbeit auszulagern- Den Rückkehrort mit
ConfigureAwait(false)entfernen - Den UI-Thread mit
.Result/.Wait()blockieren
2.2. Die erste Entscheidungstabelle
| Situation | Was während der Wartezeit läuft | Fortsetzung nach await |
Darf die UI direkt berührt werden? | Erste Wahl |
|---|---|---|---|---|
await SomeIoAsync() in einem UI-Handler |
Wartet auf den Abschluss der I/O. Der UI-Thread selbst kann zur Nachrichtenschleife zurückkehren | Im Wesentlichen der UI-Thread | Ja | plain await |
await Task.Run(...) in einem UI-Handler |
Schwere CPU-Arbeit läuft im ThreadPool | Im Wesentlichen der UI-Thread | Ja | Task.Run nur für CPU |
await x.ConfigureAwait(false) in einem UI-Handler |
Der Rückkehrort ist nicht an die UI gebunden | Ein beliebiger Thread | Nein | In UI-Code grundsätzlich vermeiden |
x.Result / x.Wait() auf dem UI-Thread |
Der UI-Thread ist durch das Warten blockiert | Die Fortsetzung kann von vornherein kaum laufen | Nein | Nicht verwenden |
UI-Update nach einem Hintergrundthread oder ConfigureAwait(false) gewünscht |
Läuft auf einem anderen Thread als der UI | Ist so, wie es ist, nicht die UI | Nein | Dispatcher.InvokeAsync / BeginInvoke / InvokeAsync |
| Eine allgemeine, UI-unabhängige Bibliothek schreiben | Unabhängig von den Umständen des Aufrufers | Erzwingt keine Rückkehr zur UI | So gestalten, dass die UI nicht berührt wird | ConfigureAwait(false) erwägen |
| Aus einem Konstruktor oder einer synchronen Eigenschaft heraus async aufrufen wollen | Der UI-Thread gerät leicht ins Warten | Der Startpfad blockiert leicht | Nein | Auf Loaded / Shown / InitializeAsync verlagern |
Wichtig an dieser Tabelle ist, dass plain await in UI-Code tatsächlich Ihr Verbündeter ist.
Der Feind ist nicht await selbst, sondern das synchrone Blockieren des UI-Threads.
Die eigentliche Entscheidung ist in dieser Tabelle bereits zusammengefasst. Die folgenden Kapitel teilen sich so auf: warum diese Tabelle so aussieht (Kapitel 3 und 4), wie Sie die Werkzeuge zur Rückkehr zur UI wählen (Kapitel 5) und wie Sie das bei Reviews erkennen (Kapitel 6 und 7).
3. In diesem Artikel verwendete Begriffe
3.1. Der UI-Thread und die Nachrichtenschleife
Die Oberfläche von WPF / WinForms funktioniert grundsätzlich so, dass es einen einzigen UI-Thread gibt, der Eingaben, Zeichnen und Ereignisverarbeitung abwickelt.
Die Aufgaben dieses UI-Threads sind ungefähr diese.
- Nachrichten wie Tastendrücke, Tastatureingaben und Neuzeichnungen verarbeiten
- Der einzige Thread sein, der Steuerelemente und UI-Objekte sicher berühren darf
- Wird er mit zu viel Arbeit vollgestopft, stocken Bildschirmaktualisierung und Reaktion auf Eingaben
Der entscheidende Punkt hier: Die Aufgabe des UI-Threads ist es, schnell zu zirkulieren. Blockieren Sie ihn längere Zeit, stauen sich Maus, Tastatur und Neuzeichnen – für den Benutzer sieht das wie „eingefroren“ aus.
Dieses Bild lässt sich als Diagramm leichter im Kopf behalten und verwirrt weniger.
flowchart LR
A["Benutzereingabe / Neuzeichnungsanforderung"] --> B["Nachrichtenschleife des UI-Threads"]
B --> C["Ereignishandler ausführen"]
C --> D["Bildschirm aktualisieren"]
D --> B
C --> E["Lange synchrone Verarbeitung"]
E --> F["Nachrichtenschleife läuft nicht mehr um"]
F --> G["Bildschirm wirkt eingefroren"]
3.2. SynchronizationContext / Dispatcher / Invoke
Die hier häufig vorkommenden Begriffe lassen sich für die Praxis so unterscheiden.
| Begriff | Bedeutung hier |
|---|---|
| UI-Thread | Der Thread, der die UI-Objekte erzeugt hat. Grundsätzlich der einzige, der die UI sicher berühren darf |
| Nachrichtenschleife | Der Mechanismus, mit dem der UI-Thread Nachrichten der Reihe nach verarbeitet |
SynchronizationContext |
Eine Abstraktion, um „die Verarbeitung an diesen Ausführungsort zurückzugeben“ |
Dispatcher |
Die Warteschlange von WPF für den UI-Thread |
Invoke / BeginInvoke / InvokeAsync |
APIs, um Arbeit an den UI-Thread zu übergeben |
Etwas genauer beschrieben, entscheidet sich der Rückkehrort so: Was await (entsprechend dem Standard ConfigureAwait(true)) einfängt, ist zunächst SynchronizationContext.Current. Nur wenn dieser null ist, wird TaskScheduler.Current betrachtet, und falls dieser nicht TaskScheduler.Default ist, kehrt die Fortsetzung zu diesem TaskScheduler zurück. Ist keines von beidem der Fall – also SynchronizationContext.Current ist null und TaskScheduler.Current ist der Standard –, läuft die Fortsetzung im ThreadPool.
Auf dem UI-Thread von WPF / WinForms trifft der erste Fall zu, das heißt, der SynchronizationContext der UI ist gesetzt. In der Praxis genügt es daher, davon auszugehen, dass der SynchronizationContext der UI wirksam ist.
Die Zuordnung je Framework lässt sich am besten als Tabelle darstellen.
| Framework | Kontext auf UI-Seite | Repräsentative APIs für die explizite Rückkehr zur UI |
|---|---|---|
| WPF | DispatcherSynchronizationContext |
Dispatcher.InvokeAsync / Dispatcher.BeginInvoke / Dispatcher.Invoke |
| WinForms | WindowsFormsSynchronizationContext |
Control.BeginInvoke / Control.Invoke / .NET 9+ Control.InvokeAsync |
Bei WPF steht der Dispatcher im Mittelpunkt.
Bei WinForms stehen das Handle des Steuerelements und die Nachrichtenschleife im Mittelpunkt, wobei BeginInvoke / Invoke in Erscheinung treten.
In der Praxis verwechseln Sie Abstraktion und konkrete Umsetzung seltener, wenn Sie sich die Beziehung etwa in dieser Form merken.
flowchart TD
A["Aktueller Code"] --> B["SynchronizationContext"]
B --> C["WPF: DispatcherSynchronizationContext"]
B --> D["WinForms: WindowsFormsSynchronizationContext"]
C --> E["Dispatcher.InvokeAsync / BeginInvoke / Invoke"]
D --> F["Control.BeginInvoke / Invoke / InvokeAsync(.NET 9+)"]
4. Typische Muster
4.1. Plain await in einem UI-Ereignishandler
Das ist die einfachste Form.
private async void LoadButton_Click(object sender, RoutedEventArgs e)
{
LoadButton.IsEnabled = false;
StatusText.Text = "Wird geladen …";
try
{
string text = await File.ReadAllTextAsync(FilePathTextBox.Text);
PreviewTextBox.Text = text;
StatusText.Text = "Fertig";
}
catch (Exception ex)
{
StatusText.Text = ex.Message;
}
finally
{
LoadButton.IsEnabled = true;
}
}
In diesem Code beginnt LoadButton_Click auf dem UI-Thread.
Und da await File.ReadAllTextAsync(...) ein plain await ist, fängt es normalerweise den UI-Kontext zu diesem Zeitpunkt ein.
Daraus ergibt sich:
- Während der Datei-I/O-Wartezeit wird der UI-Thread nicht belegt
- Die Fortsetzung nach dem Abschluss des Lesevorgangs kehrt grundsätzlich zum UI-Thread zurück
PreviewTextBox.Text = text;lässt sich direkt so schreiben
Hier ist kein zusätzlicher Dispatcher nötig.
Wenn Sie in einem UI-Handler lediglich ein plain await gemacht haben, können Sie die UI normalerweise direkt berühren.
Diese Handler-Methode ist ein async void, weil die Signatur von UI-Ereignishandlern void verlangt – hier ist das ausnahmsweise erlaubte async void. Genau deshalb gibt es einen klaren Grund, try / catch innerhalb der Methode zu platzieren. Bei async Task landet eine Ausnahme im zurückgegebenen Task und kann vom Aufrufer beim await abgefangen werden. Bei async void gibt es diesen Task nicht; eine nach außen durchgereichte Ausnahme wird an den SynchronizationContext zurückgeworfen, unter dem der Handler gestartet wurde – also an den UI-Thread. Eine unbehandelte Ausnahme auf dem UI-Thread landet bei WPF in Application.DispatcherUnhandledException, bei WinForms in Application.ThreadException, und wenn sie dort nicht behandelt wird, stürzt die Anwendung ab.
Mit anderen Worten: In einem async void-Handler ist es die Grundregel, die Ausnahme innerhalb des Handlers abzufangen. Wird sie wie im obigen Beispiel „in eine Statusanzeige verwandelt und im finally die Schaltfläche zurückgesetzt“, schließt sich das als UI-Verhalten natürlich ab. Der globale Auffangmechanismus der Anwendung (DispatcherUnhandledException und Ähnliches) ist ausschließlich als letztes Sicherheitsnetz gedacht.
Auch in WinForms ist die Sichtweise dieselbe.
Solange Sie im Click-Handler ein plain await machen, kehrt die Fortsetzung grundsätzlich zur UI-Seite zurück.
Als Diagramm sieht der Ablauf so aus.
sequenceDiagram
participant UI as UI-Thread
participant IO as Asynchrone I/O
participant Ctx as UI SynchronizationContext
UI->>UI: Click-Handler startet
UI->>IO: await ReadAllTextAsync
UI-->>Ctx: Reservierung, die Fortsetzung zur UI zurückzugeben
Note over UI: Kehrt während der Wartezeit zur Nachrichtenschleife zurück
IO-->>Ctx: I/O abgeschlossen
Ctx-->>UI: Fortsetzung auf dem UI-Thread wieder aufnehmen
UI->>UI: TextBox / Label aktualisieren
4.2. Task.Run nur für schwere CPU-Berechnungen
Task.Run zahlt sich aus, wenn schwere CPU-Berechnung vom UI-Thread entfernt werden soll.
private async void HashButton_Click(object sender, RoutedEventArgs e)
{
HashButton.IsEnabled = false;
ResultText.Text = "Wird berechnet …";
try
{
byte[] data = await File.ReadAllBytesAsync(InputPathTextBox.Text);
string hash = await Task.Run(() =>
{
using SHA256 sha256 = SHA256.Create();
byte[] digest = sha256.ComputeHash(data);
return Convert.ToHexString(digest);
});
ResultText.Text = hash;
}
catch (Exception ex)
{
ResultText.Text = ex.Message;
}
finally
{
HashButton.IsEnabled = true;
}
}
Was in diesem Code passiert, ist ungefähr dies.
- Der Ereignishandler beginnt auf dem UI-Thread
- Die I/O-Wartezeit von
File.ReadAllBytesAsyncläuft asynchron ab - Nur die schwere Hash-Berechnung wird per
Task.Runan den ThreadPool ausgelagert - Die Fortsetzung von
await Task.Run(...)ist ein plainawait, kehrt also zum UI-Thread zurück ResultText.Text = hash;lässt sich direkt so schreiben
Mit anderen Worten: Nur das Innere von Task.Run läuft auf einem anderen Thread.
Es geht nicht dauerhaft über den await hinaus „an einen Ort, der nicht mehr die UI ist“.
Auf einen Blick betrachtet ist das schwer misszuverstehen.
sequenceDiagram
participant UI as UI-Thread
participant IO as Asynchrone I/O
participant Pool as ThreadPool
UI->>IO: await ReadAllBytesAsync
IO-->>UI: Plain await, setzt sich also auf der UI fort
UI->>Pool: Schwere CPU-Verarbeitung per Task.Run auslagern
Pool-->>UI: Berechnetes Ergebnis zurückgeben
Note over UI: Die Fortsetzung von await Task.Run(...) setzt sich auf der UI fort
UI->>UI: Ergebnis im Bildschirm anzeigen
Hier gibt es zwei Dinge, auf die zu achten ist.
- I/O-Wartezeiten nicht in
Task.Runverpacken Task.Runnicht als „Asynchronisierung“, sondern als Mittel zum Schaffen einer „Auslagerungsstelle für die CPU“ betrachten
Ein Code wie Task.Run(async () => await File.ReadAllTextAsync(...)) reicht eine I/O-Wartezeit lediglich unnötig erneut an den ThreadPool weiter und bringt wenig Nutzen.
4.3. ConfigureAwait(false) bedeutet „keine erzwungene Rückkehr“, nicht „garantiert keine Rückkehr“
Das ist der Punkt, der am häufigsten missverstanden wird.
Zunächst: ConfigureAwait(false) eignet sich für allgemeinen Bibliothekscode, der nicht von der UI oder einem bestimmten Anwendungsmodell abhängt.
public sealed class DocumentRepository
{
public async Task<string> LoadNormalizedTextAsync(string path, CancellationToken cancellationToken)
{
string text = await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false);
return text.Replace("\r\n", "\n", StringComparison.Ordinal);
}
}
Diese Methode berührt die UI nicht.
Sie funktioniert gleichermaßen in WPF, WinForms, ASP.NET Core oder einem Worker.
Für Code dieser Art ist es naheliegend, ConfigureAwait(false) zu setzen.
Und der Aufruf auf UI-Seite kann ein plain await sein.
private readonly DocumentRepository _repository = new();
private async void OpenButton_Click(object sender, RoutedEventArgs e)
{
OpenButton.IsEnabled = false;
StatusText.Text = "Wird geladen …";
try
{
string text = await _repository.LoadNormalizedTextAsync(
PathTextBox.Text,
CancellationToken.None);
PreviewTextBox.Text = text;
StatusText.Text = "Fertig";
}
catch (Exception ex)
{
StatusText.Text = ex.Message;
}
finally
{
OpenButton.IsEnabled = true;
}
}
Wichtig ist hier, dass das ConfigureAwait(false) innerhalb der Bibliothek das await des Aufrufers nicht zwangsweise ebenfalls auf false setzt.
Es entsteht also folgende Trennung:
- Innerhalb der Bibliothek kehrt die Ausführung nicht zur UI zurück
- Wenn der UI-Handler sie mit einem plain
awaitaufruft, kehrt die Fortsetzung beim Aufrufer zur UI zurück
Umgekehrt ist es gefährlich, wenn der UI-Handler selbst so geschrieben wird.
private async void OpenButton_Click(object sender, RoutedEventArgs e)
{
string text = await _repository.LoadNormalizedTextAsync(
PathTextBox.Text,
CancellationToken.None).ConfigureAwait(false);
PreviewTextBox.Text = text;
}
In diesem Fall wird die Fortsetzung dieses await in OpenButton_Click nicht gezwungen, zur UI zurückzukehren.
Daher kann PreviewTextBox.Text = text; zu einem Cross-Thread-Zugriff werden.
Es gibt noch einen weiteren, unscheinbaren, aber wichtigen Punkt.
ConfigureAwait(false) garantiert keinen Wechsel zum ThreadPool: Wenn dieses await ohne Warten sofort abgeschlossen wird, kann die Fortsetzung einfach auf dem aktuellen Thread weiterlaufen.
Es als „geht immer auf einen anderen Thread“ oder „ab hier ist es nie mehr die UI“ zu lesen, ist ein Rezept für Fehler. Die Bedeutung ist ausschließlich diese: Die Fortsetzung dieses await wird nicht gezwungen, zum ursprünglichen UI-Kontext zurückzukehren – nicht mehr.
Als Diagramm sieht das so aus.
flowchart LR
A["await in einem UI-Handler"] --> B{"ConfigureAwait(false) setzen?"}
B -- Nein --> C["Fortsetzung grundsätzlich auf dem UI-Thread"]
C --> D["UI lässt sich leicht direkt aktualisieren"]
B -- Ja --> E["Fortsetzung nicht an die UI gebunden"]
E --> F["Kann auf beliebigem Thread wieder aufgenommen werden"]
F --> G["UI-Update benötigt Dispatcher / Invoke"]
4.4. Warum .Result / .Wait() / .GetAwaiter().GetResult() blockieren
Das ist der am häufigsten anzutreffende Unfall.
private void LoadButton_Click(object sender, RoutedEventArgs e)
{
string text = LoadTextAsync().Result;
PreviewTextBox.Text = text;
}
private async Task<string> LoadTextAsync()
{
string text = await File.ReadAllTextAsync(FilePathTextBox.Text);
return text.ToUpperInvariant();
}
Auf den ersten Blick sieht es so aus, als würde hier nur synchron ein Ergebnis abgeholt, aber auf dem UI-Thread ist das gefährlich.
Der Ablauf als Diagramm sieht so aus.
sequenceDiagram
participant UI as UI-Thread
participant IO as Asynchrone I/O
participant Ctx as UI SynchronizationContext
UI->>UI: LoadButton_Click startet
UI->>IO: LoadTextAsync() aufrufen
IO-->>UI: Gibt einen unabgeschlossenen Task zurück
UI->>UI: Blockiert wartend bei .Result
IO-->>Ctx: I/O abgeschlossen, will die Fortsetzung zur UI zurückgeben
Ctx-->>UI: Möchte die Fortsetzung ausführen
Note over UI: Aber die UI ist durch .Result blockiert
Note over UI, Ctx: Die Fortsetzung kann nicht laufen, kann also nicht abgeschlossen werden
In Worten gefasst, passiert Folgendes.
- Der UI-Thread ruft
LoadTextAsync()auf - Das
awaitinnerhalb vonLoadTextAsync()fängt den UI-Kontext ein - Der UI-Thread wartet bei
.Result - Die I/O wird abgeschlossen
- Die Fortsetzung von
LoadTextAsync()möchte zum UI-Thread zurückkehren - Aber der UI-Thread ist durch
.Resultblockiert - Da die Fortsetzung nicht laufen kann, wird
LoadTextAsync()nicht abgeschlossen .Resultkehrt nie zurück
Mit anderen Worten: Die UI sagt „ich warte, bis du fertig bist“, und die asynchrone Seite sagt „ich kann erst fertig werden, wenn ich zur UI zurückkehren kann“ – beide warten aufeinander. Wirklich unangenehm.
Ein verbreiteter Irrtum ist die Annahme, GetAwaiter().GetResult() sei sicher.
Doch das Wesentliche – das Blockieren des UI-Threads – bleibt dasselbe. Unterschiedlich ist im Wesentlichen nur, wie die Ausnahme verpackt wird.
In der UI ist es deshalb sicherer, diese drei als dieselbe Gefahr zu behandeln.
.Result.Wait().GetAwaiter().GetResult()
Aus demselben Grund ist es auch gefährlich, den Task der DispatcherOperation, den Dispatcher.InvokeAsync(...) von WPF zurückgibt, vom UI-Thread aus mit Task.Wait() abzuwarten. InvokeAsync reiht den übergebenen Delegaten lediglich in die Warteschlange des Dispatcher ein; tatsächlich läuft er erst, wenn der UI-Thread diese Warteschlange abarbeitet. Ist der UI-Thread durch Wait() angehalten, läuft die Warteschlange nicht ab, sodass dieser Task niemals abgeschlossen wird.
Dasselbe gilt auch auf Seite der DispatcherOperation: Für DispatcherOperation.Wait() ist ausdrücklich dokumentiert, dass es eine InvalidOperationException auslöst, wenn ein auf demselben Thread laufender Vorgang abgewartet wird. Der blockierende Wartepfad selbst ist also gar nicht vorgesehen.
Im UI-Kontext ist es die gesamte Richtung „synchron auf etwas Gepostetes warten“, die leicht zu Blockaden führt. Eine ausführliche Erklärung der Blockade-Mechanik findet sich gut lesbar unter Await, and UI, and deadlocks! Oh my!.
Führt das „garantiert“ zu einem Deadlock? Nicht unbedingt. Bei Code, dessen Fortsetzung zufällig nicht zur UI zurückkehrt, kann es sein, dass die UI ohne Deadlock einfach nur einfriert. Auch das ist jedoch schmerzhaft genug – lassen Sie es in der UI grundsätzlich sein.
5. Wann Dispatcher / Invoke verwenden
Vor diesem Hintergrund: In einem UI-Handler mit plain await sind normalerweise keine expliziten Dispatcher / Invoke nötig.
Nötig wird es zum Beispiel in diesen Fällen.
- Die UI soll in der Fortsetzung nach
ConfigureAwait(false)berührt werden - Innerhalb von
Task.Run, oder wenn auch außerhalb bewusst so aufgebaut ist, dass es nicht zur UI zurückkehrt - Benachrichtigungen kommen von vornherein auf einem Nicht-UI-Thread an – Socket-Empfang, Timer, Ereignis-Callbacks
- In einer Schicht, die UI und Nicht-UI absichtlich trennt, soll nur das abschließende UI-Update explizit gemacht werden
In WPF ist Dispatcher.InvokeAsync das repräsentative Mittel.
private async Task RefreshPreviewAsync(string path, CancellationToken cancellationToken)
{
string text = await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false);
await Dispatcher.InvokeAsync(() =>
{
PreviewTextBox.Text = text;
StatusText.Text = "Fertig";
});
}
In WinForms ab .NET 9 fügt sich InvokeAsync sauber in den async-Ablauf ein.
private async Task RefreshPreviewAsync(string path, CancellationToken cancellationToken)
{
string text = await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false);
await previewTextBox.InvokeAsync(() =>
{
previewTextBox.Text = text;
statusLabel.Text = "Fertig";
});
}
Im klassischen WinForms-Muster verwenden Sie BeginInvoke.
Invoke ist ein synchrones Senden und lässt den Aufrufer warten. BeginInvoke postet und kehrt sofort zurück.
In einem async-Ablauf passt in der Regel die nicht blockierende Seite besser.
Control.BeginInvoke gibt allerdings ein IAsyncResult zurück, das sich nicht direkt awaiten lässt. Wollen Sie in einer Umgebung ohne Control.InvokeAsync (.NET Framework 4.8, .NET 6 / 8 und Ähnliches) trotzdem im async-Ablauf bleiben, liegt es nahe, es mit einer TaskCompletionSource in einen Task zu verpacken.
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
public static class ControlUiExtensions
{
// Damit das auch unter .NET Framework 4.8 unverändert funktioniert,
// wird hier die generische TaskCompletionSource verwendet. Ab .NET 5
// ließe sich das auch mit der nicht generischen Version schreiben.
//
// cancellationToken wurde bewusst nicht optional gemacht. Wird die
// Steuerelementinstanz zerstört, nachdem BeginInvoke sie angenommen hat,
// wird der geposteten Delegat verworfen, ohne ausgeführt zu werden, und
// die TaskCompletionSource erhält weder Ergebnis noch Ausnahme. Ohne einen
// Abbruchweg würde die await-Seite dann für immer warten
public static Task InvokeOnUiAsync(
this Control control, Action action, CancellationToken cancellationToken)
{
if (control is null)
{
throw new ArgumentNullException(nameof(control));
}
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}
if (!control.IsHandleCreated)
{
throw new InvalidOperationException("Das Fensterhandle wurde noch nicht erstellt.");
}
if (!control.InvokeRequired)
{
action();
return Task.CompletedTask;
}
// Damit die Fortsetzung der await-Seite nicht einfach auf dem
// UI-Thread weiterläuft, wird hier ausdrücklich festgelegt,
// dass die Fortsetzung asynchron fließt.
var tcs = new TaskCompletionSource<bool>(
TaskCreationOptions.RunContinuationsAsynchronously);
// Abbruch und Ausführung kämpfen um dasselbe "einmalige Recht".
// Mit Interlocked.Exchange kommt nur die Seite weiter, die zuerst
// eine 1 schreiben konnte.
// Bei einer Schreibweise, die erst das Flag prüft und dann action()
// aufruft, bleibt der Pfad offen, dass unmittelbar nach der Prüfung
// ein Abbruch eintritt: Die await-Seite hat den Abbruch erhalten und
// beginnt bereits die nächste Operation, während der noch alte,
// in der Warteschlange verbliebene Delegat später doch noch die
// Oberfläche überschreibt
int claimed = 0; // 0 = noch offen / 1 = eine Seite hat es sich geholt
// Wird abgebrochen, wird der Task auch dann abgeschlossen, wenn der
// Delegat nicht ausgeführt wird. Die Registrierung wird beim
// Abschluss des Task unbedingt wieder entfernt (sonst hält sie tcs
// fest, solange das Token lebt). CancellationTokenRegistration.Dispose
// ist threadsicher und darf von jedem Thread aus aufgerufen werden
CancellationTokenRegistration registration = cancellationToken.Register(() =>
{
if (Interlocked.Exchange(ref claimed, 1) == 0)
{
tcs.TrySetCanceled(cancellationToken);
}
});
tcs.Task.ContinueWith(
_ => registration.Dispose(),
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
try
{
control.BeginInvoke(new Action(() =>
{
// Zwischen dem Posten und dem Zeitpunkt, an dem der UI-Thread
// tätig wird, kann inzwischen abgebrochen worden sein. Kann
// hier das Recht nicht mehr erlangt werden, hat sich die
// Abbruchseite bereits durchgesetzt – dann wird die Oberfläche
// gar nicht erst berührt, und die Methode kehrt zurück
if (Interlocked.Exchange(ref claimed, 1) != 0)
{
return;
}
try
{
action();
tcs.TrySetResult(true);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
}));
}
catch (Exception ex)
{
// BeginInvoke selbst kann eine Ausnahme werfen (etwa wenn das
// Handle bereits fehlt). Wird das hier nicht abgeschlossen,
// bleibt es wieder bei einem endlosen Warten.
// Der Delegat läuft in diesem Fall nicht, daher wird auch hier
// erst das Recht geholt und dann abgeschlossen
if (Interlocked.Exchange(ref claimed, 1) == 0)
{
tcs.TrySetException(ex);
}
}
return tcs.Task;
}
}
Der Aufruf sieht auf der Aufruferseite fast genauso aus wie im InvokeAsync-Beispiel. Binden Sie das Token an die Lebensdauer des Formulars.
// Feld des Formulars. Wird beim Schließen abgebrochen
private readonly CancellationTokenSource _formClosing = new();
protected override void OnFormClosed(FormClosedEventArgs e)
{
// Damit sich die await-Seite auch dann abschließen lässt, wenn ein
// bereits gepostet Delegat verworfen wird, ohne ausgeführt zu werden
_formClosing.Cancel();
base.OnFormClosed(e);
}
private async Task RefreshPreviewAsync(string path, CancellationToken cancellationToken)
{
using var linked = CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken, _formClosing.Token);
string text = await File.ReadAllTextAsync(path, linked.Token).ConfigureAwait(false);
await previewTextBox.InvokeOnUiAsync(() =>
{
previewTextBox.Text = text;
statusLabel.Text = "Fertig";
}, linked.Token);
}
In dieser Form lassen sich auch Ausnahmen, die in der UI auftreten, im try / catch an der Stelle abfangen, an der awaitet wurde. Vier Punkte sind dabei festzuhalten.
- Abbruch und Ausführung reichen nicht aus, wenn sie „erst das Flag prüfen, dann handeln“. Unmittelbar nachdem geprüft wurde, „ist schon abgebrochen worden?“, kann noch vor dem Aufruf von
action()ein Abbruch eintreten. In diesem einen Augenblick wirdtcsals abgebrochen markiert, und die wartende Aufruferseite geht weiter und beginnt die nächste Operation. Danach läuft der noch in der Warteschlange verbliebene alte Delegat und überschreibt die Oberfläche – die neue Anzeige wird von der alten überschrieben, ein Fehler, der sich nur schwer reproduzieren lässt. Genau deshalb lässt der obige CodeInterlocked.Exchangeum „das einmalige Recht“ konkurrieren; die Seite, die es nicht bekommt, kehrt zurück, ohne etwas zu tun - Ein
BeginInvokevor der Handle-Erstellung (vorLoad) oder nach dem Schließen des Formulars löst eine Ausnahme aus. Behalten Sie die Lebensdauer der aufrufenden Seite im Blick - Wird das Steuerelement nach dem Posten zerstört, kann der Delegat verworfen werden, ohne ausgeführt zu werden. In diesem Fall erhält die
TaskCompletionSourceweder Ergebnis noch Ausnahme, sodass die wartende await-Seite auf unbestimmte Zeit wartet. Übergeben Sie deshalb wie im obigen Beispiel unbedingt ein an das Ende des Formulars gebundenes Token. Das Ergebnis eines Abbruchs steigt dann alsOperationCanceledExceptionauf File.ReadAllTextAsyncist eine API ab .NET Core 2.0. Wollen Sie dieselbe Form unter .NET Framework 4.8 abbilden, ersetzen Sie es zum Beispiel durchStreamReader.ReadToEndAsync
Für die Unterscheidung reicht diese Grobeinteilung.
| Ziel | WPF | WinForms |
|---|---|---|
| Synchron in die UI einreihen | Dispatcher.Invoke |
Control.Invoke |
| Asynchron an die UI posten | Dispatcher.InvokeAsync / Dispatcher.BeginInvoke |
Control.BeginInvoke / .NET 9+ Control.InvokeAsync |
| Sich sauber in async / await einfügen | Dispatcher.InvokeAsync |
.NET 9+ Control.InvokeAsync, davor BeginInvoke |
Als praktisches Gespür gilt:
- Nicht nötig, wenn im UI-Handler nur plain
awaitverwendet wird - Verwenden, wenn die UI von einem Ort berührt werden soll, der nicht die UI ist
- Synchrones
Invokeinnerhalb von async-Abläufen nicht übermäßig vermehren
Allein damit lassen sich schon viele Unfälle vermeiden.
Im Zweifel genügt ein Entscheidungsdiagramm auf diesem Niveau.
flowchart TD
A["Läuft diese Fortsetzung auf dem UI-Thread?"] --> B{"Ja?"}
B -- Ja --> C["Beim plain await bleiben und die UI aktualisieren"]
B -- Nein --> D{"Soll die UI berührt werden?"}
D -- Nein --> E["Verarbeitung wie gewohnt fortsetzen"]
D -- Ja --> F["WPF: Dispatcher.InvokeAsync"]
D -- Ja --> G["WinForms: BeginInvoke / InvokeAsync"]
6. Häufige Antipatterns
| Antipattern | Warum es wehtut | Erste Ersatzlösung |
|---|---|---|
LoadAsync().Result in einem UI-Handler |
Blockiert den UI-Thread. Anfällig für Deadlocks | await LoadAsync() |
LoadAsync().Wait() in einem UI-Handler |
Dasselbe. Die Nachrichtenschleife bleibt stehen | await LoadAsync() |
LoadAsync().GetAwaiter().GetResult() in einem UI-Handler |
Nur die Ausnahmedarstellung unterscheidet sich, das Blockieren ist gleich | await LoadAsync() |
ConfigureAwait(false) mechanisch an UI-Code anhängen |
UI-Updates nach await brechen leicht |
Plain await in der äußersten UI-Schicht |
Task.Run(async () => await IoAsync()) |
Postet die I/O unnötig erneut | await IoAsync() |
Bibliothekscode hält Dispatcher oder Control direkt |
Vertieft die UI-Abhängigkeit. Schwer wiederzuverwenden | Bibliothek liefert nur Daten, die UI-Seite übernimmt das Marshalling |
Dispatcher.Invoke / Control.Invoke übermäßig in async-Abläufen verwenden |
Bildet leicht Ringe von Blockaden | Dispatcher.InvokeAsync / BeginInvoke / InvokeAsync erwägen |
| async in Konstruktoren oder Eigenschaften-Gettern synchronisieren | Nährboden für Hänger beim Start | Auf Loaded / Shown / InitializeAsync verlagern |
Unter diesen sind besonders drei häufig anzutreffen.
.Result/.Wait()auf dem UI-ThreadConfigureAwait(false)mechanisch an UI-Code anhängen- Zuständigkeiten von Bibliothek und UI vermischen sich, sodass der
Dispatchertief eindringt
Allein das Entfernen dieser drei Muster beruhigt den Code schon erheblich.
7. Checkliste für Reviews
Der Inhalt entspricht der Entscheidungstabelle aus 2.2 und den Antipatterns aus Kapitel 6, ist hier aber als Fragen formuliert, die Sie beim Öffnen des Codes der Reihe nach prüfen.
- Ist in UI-Ereignishandlern oder UI-Initialisierungspfaden noch
.Result/.Wait()/.GetAwaiter().GetResult()übrig geblieben? - Wird
Task.Runausschließlich für CPU-Berechnung verwendet? Wird damit keine I/O verpackt? - Ist
ConfigureAwait(false)mechanisch in UI-Code eingeflossen? - Schleppt umgekehrt eine allgemeine Bibliothek eine Abhängigkeit vom UI-Kontext mit sich?
- Lässt sich an jeder Stelle, an der nach
awaitdirekt die UI berührt wird, tatsächlich belegen, dass sie sich im UI-Kontext befindet? - Werden dort, wo eine explizite Rückkehr zur UI nötig ist,
Dispatcher.InvokeAsync/BeginInvoke/InvokeAsyncverwendet? - Vermehren sich synchrone Marshalling-Aufrufe wie
Dispatcher.Invoke/Control.Invokeunnötig? - Wird async aus Konstruktoren, synchronen Eigenschaften oder synchronen Ereignissen gewaltsam synchronisiert?
- Greift die Bibliotheksschicht direkt auf
Window/Control/Dispatcherzu?
Diese Checkliste eignet sich auch gut dafür, im Team abzustimmen, „was zur Zuständigkeit der UI gehört“.
8. Grobe Entscheidungshilfe
Die situationsabhängige Wahl ist bereits in der Entscheidungstabelle aus 2.2 zusammengefasst; hier bleibt nur die Merkregel zum Mitnehmen.
- Die äußerste UI-Schicht bleibt bei plain
await. Dass sich nachawaitdirekt die UI berühren lässt, liegt genau daran, dass diese Regel eingehalten wird Task.Runist die Auslagerungsstelle für die CPU. Es ist kein Werkzeug, um I/O-Wartezeiten zu verpackenConfigureAwait(false)ist ein Werkzeug für allgemeine Bibliotheken. Nicht mechanisch an UI-Code anhängenDispatcher/BeginInvoke/InvokeAsyncnur, wenn die UI von einem Ort berührt werden muss, der nicht die UI ist- Die drei Wartemittel auf dem UI-Thread (
.Result/.Wait()/.GetAwaiter().GetResult()) nicht verwenden. Wollen Sie synchronisieren, ziehen Sie stattdessen die gesamte Aufruferkette zu async hoch
Die Begründung finden Sie in Kapitel 4, die Wahl bei Dispatcher / Invoke in Kapitel 5, und die Anhaltspunkte zum Auffinden im tatsächlichen Code in den Kapiteln 6 und 7.
9. Zusammenfassung
Was bei async / await in WPF / WinForms wirklich zählt, ist nicht die vage Stimmung „Asynchronität ist schwierig“, sondern getrennt zu betrachten:
- Wo etwas begonnen hat
- Wohin die Fortsetzung von
awaitzurückkehrt - Wer die Verantwortung trägt, zur UI zurückzukehren
Als erste Grundregeln reicht es, genau diese zu befolgen:
- Die äußerste UI-Schicht: plain
await - Nur schwere CPU-Arbeit:
Task.Run - In allgemeinen Bibliotheken:
ConfigureAwait(false)erwägen - Nur wenn eine Rückkehr zur UI nötig ist:
Dispatcher/BeginInvoke/InvokeAsync - Auf dem UI-Thread:
.Result/.Wait()/.GetAwaiter().GetResult()nicht verwenden
async / await selbst ist kein besonders launischer Mechanismus.
Nur wenn Sie ihn verwenden, ohne den UI-Thread als Mittelpunkt im Blick zu behalten, wird es plötzlich zum Morast.
Umgekehrt gilt:
- Außen und innen der UI trennen
- Sich des Rückkehrorts bewusst bleiben
- Keine Blockaden hereinholen
Halten Sie sich an genau diese drei Punkte, wird der asynchrone Code in WPF / WinForms deutlich ruhiger. Code, der die Oberfläche einfrieren lässt, liegt meist nicht daran, dass „Asynchronität schlecht“ wäre, sondern schlicht daran, dass die Art, sich beim UI-Thread zu verschulden, unsauber ist.
10. Referenzen
- Vollständiges Beispielcode-Paket zu diesem Artikel (UI-unabhängige Bibliothek, WPF- / WinForms-Beispiele, Unittests) - komurasoft-blog-samples (GitHub)
- Verwandter Artikel: C# async/await-Praxis-Entscheidungstabelle – Task.Run und ConfigureAwait
- Threading Model - WPF
- DispatcherSynchronizationContext Class
- How to handle cross-thread operations with controls - Windows Forms
- WindowsFormsSynchronizationContext Class
- Events Overview - Windows Forms
- TaskScheduler.FromCurrentSynchronizationContext Method
- ConfigureAwait FAQ
- How Async/Await Really Works in C#
- Await, and UI, and deadlocks! Oh my!
- Threading model for WebView2 apps
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
CI/CD für WinForms-/WPF-Anwendungen in der Praxis ── Vom Build über die Signierung bis zur Distribution mit GitHub Actions automatisieren
Ein praktischer Leitfaden zur Einrichtung von CI/CD für WinForms-/WPF-Anwendungen mit GitHub Actions. Behandelt eine minimale YAML für Bu...
Infobereich-Symbole und Toast-Benachrichtigungen in Windows-Apps — Fallstricke von NotifyIcon und die richtige AppNotification-Wahl
Ein praktischer Leitfaden dafür, eine geschäftliche Windows-Anwendung im Infobereich (System Tray) resident zu halten und den Benutzer üb...
Mehrsprachigkeit für WinForms/WPF-Anwendungen ── resx, Satelliten-Assemblies und Kulturumschaltung in der Praxis
Dieser Artikel behandelt die Mehrsprachigkeit von Windows-Desktopanwendungen aus Praxissicht: den Unterschied zwischen CurrentCulture und...
Entra-ID-Authentifizierung in WinForms/WPF-Apps integrieren — Eine praxistaugliche Architektur mit MSAL.NET und dem WAM-Broker
Ein praxisnaher Blick auf die Integration der Entra-ID-Authentifizierung (früher Azure AD) in WinForms/WPF-Desktop-Apps: das Public-Clien...
UI-Automatisierungstests für Windows-Desktop-Apps — Wie UI Automation funktioniert und wie man mit FlaUI robuste Tests baut
Ein praxisnaher Leitfaden zu UI-Automatisierungstests für WinForms-/WPF-Apps, ausgehend davon, wie Windows UI Automation selbst funktioni...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
UI-Threading und Timer
WPF-/WinForms-UI-Thread, asynchrone Abläufe, Dispatcher und Timer-Entscheidungen.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Windows-App-Entwicklung
Der UI-Thread und async/await in WPF / WinForms gehören bei der Windows-Anwendungsentwicklung zu den Punkten, an denen die Umsetzung am häufigsten ins Stocken gerät.
Technische Beratung und Design-Review
Wenn Sie die Zuständigkeiten von UI und Hintergrundverarbeitung sowie den richtigen Einsatz des Dispatchers ordnen möchten, lässt sich das im Rahmen einer technischen Beratung und eines Design-Reviews klären.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- Zu welchem Thread kehrt der Code nach await zurück?
- Bei einem plain await (ohne ConfigureAwait) in einem UI-Ereignishandler von WPF oder WinForms kehrt die Fortsetzung nach await grundsätzlich zum UI-Thread zurück. Der Grund ist, dass await den zu diesem Zeitpunkt aktuellen UI-SynchronizationContext einfängt und die Fortsetzung dorthin zurückgibt, sodass sich TextBox- oder Label-Updates nach dem await direkt schreiben lassen. Auch bei await Task.Run(...) läuft nur die eigentliche Berechnung im ThreadPool; bei einem plain await setzt sich die Fortsetzung anschließend auf dem UI-Thread fort.
- Warum blockiert .Result oder .Wait() auf dem UI-Thread?
- Weil der UI-Thread, während er bei .Result wartet, selbst blockiert ist: Die Fortsetzung der asynchronen Verarbeitung will zum eingefangenen UI-Kontext zurückkehren, kann dort aber nicht laufen, weil der UI-Thread durch .Result belegt ist – beide Seiten warten aufeinander, und es entsteht ein Deadlock. GetAwaiter().GetResult() unterscheidet sich nur in der Art, wie Ausnahmen verpackt werden; im Kern blockiert es den UI-Thread genauso. Vermeiden Sie in der UI alle drei – .Result, .Wait() und GetAwaiter().GetResult() – und verwenden Sie stattdessen await.
- Sollte ConfigureAwait(false) an UI-Code angehängt werden?
- Besser nicht. ConfigureAwait(false) bedeutet, dass die Rückkehr zum eingefangenen UI-Kontext nicht erzwungen wird; die Fortsetzung kann dann auf einem beliebigen Thread weiterlaufen, und ein direkt anschließendes UI-Update kann zu einem Cross-Thread-Zugriff werden. Es eignet sich für allgemeinen, UI-unabhängigen Bibliothekscode; die Richtlinie lautet, die äußerste UI-Schicht bei plain await zu belassen.
- Wann sollte Task.Run verwendet werden?
- Nur dann, wenn eine rechenintensive CPU-Berechnung vom UI-Thread wegverlagert werden soll. I/O-Wartezeiten in Task.Run zu verpacken bringt nichts – es wird lediglich unnötig erneut an den ThreadPool weitergereicht. Nur der Inhalt von Task.Run läuft auf einem anderen Thread; die Fortsetzung von await Task.Run(...) kehrt bei einem plain await normalerweise auf den UI-Thread zurück, sodass sich das Ergebnis direkt in der Oberfläche anzeigen lässt.
Autorenprofil
Profilseite des Artikelautors.
Go Komura
Geschäftsführer von KomuraSoft LLC
Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.