Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Multithreading kann die Leistung von Windows Forms-Apps verbessern, aber der Zugriff auf Windows Forms-Steuerelemente ist nicht von Natur aus threadsicher. Multithreading kann Ihren Code schwerwiegenden und komplexen Fehlern aussetzen. Wenn zwei oder mehr Threads ein Steuerelement ändern, wird das Steuerelement möglicherweise in einen inkonsistenten Zustand versetzt, und es kann zu Racebedingungen, Deadlocks, zum Einfrieren und zu Fehlern kommen. Wenn Sie Multithreading in Ihrer App implementieren, achten Sie darauf, Threadübergreifende Steuerelemente auf threadsichere Weise aufzurufen. Weitere Informationen finden Sie unter Bewährte Methoden für verwaltetes Threading.
Es gibt zwei Möglichkeiten, ein Windows Forms-Steuerelement aus einem Thread, der dieses Steuerelement nicht erstellt hat, sicher aufzurufen. Verwenden Sie die System.Windows.Forms.Control.Invoke Methode, um einen im Hauptthread erstellten Delegaten aufzurufen, der wiederum das Steuerelement aufruft. Oder implementieren Sie ein System.ComponentModel.BackgroundWorker, das ein ereignisgesteuertes Modell verwendet, um die im Hintergrundthread erledigte Arbeit von der Berichterstellung zu den Ergebnissen zu trennen.
Unsichere threadübergreifende Aufrufe
Es ist unsicher, ein Steuerelement direkt aus einem Thread aufzurufen, der es nicht erstellt hat. Der folgende Codeausschnitt veranschaulicht einen unsicheren Aufruf des System.Windows.Forms.TextBox-Steuerelements. Der Button1_Click-Ereignishandler erstellt einen neuen WriteTextUnsafe-Thread, der die TextBox.Text-Eigenschaft des Hauptthreads direkt festlegt.
private void button2_Click(object sender, EventArgs e)
{
WriteTextUnsafe("Writing message #1 (UI THREAD)");
_ = Task.Run(() => WriteTextUnsafe("Writing message #2 (OTHER THREAD)"));
}
private void WriteTextUnsafe(string text) =>
textBox1.Text += $"{Environment.NewLine}{text}";
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
WriteTextUnsafe("Writing message #1 (UI THREAD)")
Task.Run(Sub() WriteTextUnsafe("Writing message #2 (OTHER THREAD)"))
End Sub
Private Sub WriteTextUnsafe(text As String)
TextBox1.Text += $"{Environment.NewLine}{text}"
End Sub
Der Visual Studio-Debugger erkennt diese unsicheren Threadaufrufe, indem eine InvalidOperationException Meldung ausgelöst wird, der Crossthread-Vorgang ungültig ist. Steuerelement, auf das über einen anderen Thread als den Thread zugegriffen wurde, auf dem es erstellt wurde. Der InvalidOperationException Fehler tritt immer für unsichere Threadübergreifende Aufrufe während des Visual Studio-Debuggings auf und kann zur App-Laufzeit auftreten. Sie sollten das Problem beheben, aber Sie können die Ausnahme deaktivieren, indem Sie die eigenschaft Control.CheckForIllegalCrossThreadCalls auf falsefestlegen.
Sichere threadübergreifende Aufrufe
Windows Forms-Anwendungen folgen einem strengen vertragsähnlichen Framework, ähnlich allen anderen Windows UI-Frameworks: Alle Steuerelemente müssen erstellt und über denselben Thread aufgerufen werden. Dies ist wichtig, da Windows Anwendungen erfordert, dass ein einzelner dedizierter Thread bereitgestellt wird, an den Systemnachrichten übermittelt werden. Wenn der Windows-Fenster-Manager eine Interaktion mit einem Anwendungsfenster erkennt, z. B. ein Tastendruck, ein Mausklick oder eine Größenänderung des Fensters, leitet er diese Informationen an den Thread weiter, der die Benutzeroberfläche erstellt und verwaltet, und wandelt sie in Aktionen erfordernde Ereignisse um. Dieser Thread wird als UI-Thread bezeichnet.
Da Code, der in einem anderen Thread ausgeführt wird, nicht auf Steuerelemente zugreifen kann, die vom UI-Thread erstellt und verwaltet werden, bietet Windows Forms Möglichkeiten, mit diesen Steuerelementen sicher aus einem anderen Thread zu arbeiten, wie in den folgenden Codebeispielen gezeigt:
Beispiel: Verwenden von Control.InvokeAsync (.NET 9 und höher)
Die Control.InvokeAsync Methode (.NET 9+), die asynchrone Marshalling für den UI-Thread bereitstellt.
Beispiel: Verwenden Sie die Control.Invoke-Methode:
Die Control.Invoke-Methode, die einen Delegaten aus dem Hauptthread aufruft, um das Steuerelement aufzurufen
Beispiel: Verwenden eines BackgroundWorkers
Eine BackgroundWorker-Komponente, die ein ereignisgesteuertes Modell bietet.
Beispiel: Verwenden von Control.InvokeAsync (.NET 9 und höher)
Ab .NET 9 enthält Windows Forms die InvokeAsync Methode, die asynchrone Marshaling für den UI-Thread bereitstellt. Diese Methode ist nützlich für asynchrone Ereignishandler und beseitigt viele häufige Deadlock-Szenarien.
Hinweis
Control.InvokeAsync ist nur in .NET 9 und höher verfügbar. Sie wird in .NET Framework nicht unterstützt.
Grundlegendes zum Unterschied: Invoke vs InvokeAsync
Control.Invoke (Senden - Blockieren):
- Sendet den Delegaten synchron an die Nachrichtenwarteschlange des UI-Threads.
- Der aufrufende Thread wartet, bis der UI-Thread den Delegaten verarbeitet.
- Kann dazu führen, dass die Benutzeroberfläche eingefroren ist, wenn die Stellvertretung, die an die Nachrichtenwarteschlange gemarstet wurde, selbst auf die Eintreffen einer Nachricht (Deadlock) wartet.
- Nützlich, wenn Sie Ergebnisse für die Anzeige im UI-Thread haben, z. B.: Deaktivieren einer Schaltfläche oder Festlegen des Texts eines Steuerelements.
Control.InvokeAsync (Posting - Non-blocking):
- Sendet den Delegaten asynchron in die Nachrichtenwarteschlange des UI-Threads, anstatt auf den Abschluss des Aufrufs zu warten.
- Gibt sofort zurück, ohne den aufrufenden Thread zu blockieren.
- Gibt einen
TaskWert zurück, der für den Abschluss erwartet werden kann. - Ideal für asynchrone Szenarien und verhindert UI-Threadengpässe.
Vorteile von InvokeAsync
Control.InvokeAsync hat gegenüber der älteren Control.Invoke Methode mehrere Vorteile. Es gibt ein Task , das Sie erwarten können, sodass es gut mit asynchronem und await-Code funktioniert. Außerdem werden häufige Deadlockprobleme verhindert, die beim Mischen von asynchronem Code mit synchronen Aufrufen auftreten können. Im Gegensatz dazu Control.Invokeblockiert die InvokeAsync Methode den aufrufenden Thread nicht, wodurch Ihre Apps reaktionsfähig bleiben.
Die Methode unterstützt den Abbruch über CancellationToken, sodass Sie Vorgänge bei Bedarf abbrechen können. Außerdem werden Ausnahmen ordnungsgemäß behandelt und an Den Code übergeben, damit Fehler ordnungsgemäß behandelt werden können. .NET 9 enthält Compilerwarnungen (WFO2001), mit denen Sie die Methode richtig verwenden können.
Umfassende Anleitungen zu asynchronen Ereignishandlern und bewährten Methoden finden Sie unter "Übersicht über Ereignisse".
Auswählen der richtigen InvokeAsync-Überladung
Control.InvokeAsync bietet vier Überladungen für verschiedene Szenarien:
| Überlasten | Anwendungsfall | Example |
|---|---|---|
InvokeAsync(Action) |
Synchronisierungsvorgang, kein Rückgabewert. | Dient zum Aktualisieren von Steuerelementeigenschaften. |
InvokeAsync<T>(Func<T>) |
Synchronisierungsvorgang mit Rückgabewert. | Rufen Sie den Steuerstatus ab. |
InvokeAsync(Func<CancellationToken, ValueTask>) |
Asynchroner Vorgang, kein Rückgabewert.* | Lang ausgeführte UI-Updates. |
InvokeAsync<T>(Func<CancellationToken, ValueTask<T>>) |
Asynchroner Vorgang mit Rückgabewert.* | Asynchrone Daten werden mit Ergebnis abgerufen. |
*Visual Basic unterstützt kein Warten auf ein ValueTask.
Das folgende Beispiel veranschaulicht die Verwendung InvokeAsync der sicheren Aktualisierung von Steuerelementen aus einem Hintergrundthread:
private async void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;
try
{
// Perform background work
await Task.Run(async () =>
{
for (int i = 0; i <= 100; i += 10)
{
// Simulate work
await Task.Delay(100);
// Create local variable to avoid closure issues
int currentProgress = i;
// Update UI safely from background thread
await loggingTextBox.InvokeAsync(() =>
{
loggingTextBox.Text = $"Progress: {currentProgress}%";
});
}
});
loggingTextBox.Text = "Operation completed!";
}
finally
{
button1.Enabled = true;
}
}
Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles button1.Click
button1.Enabled = False
Try
' Perform background work
Await Task.Run(Async Function()
For i As Integer = 0 To 100 Step 10
' Simulate work
Await Task.Delay(100)
' Create local variable to avoid closure issues
Dim currentProgress As Integer = i
' Update UI safely from background thread
Await loggingTextBox.InvokeAsync(Sub()
loggingTextBox.Text = $"Progress: {currentProgress}%"
End Sub)
Next
End Function)
' Update UI after completion
Await loggingTextBox.InvokeAsync(Sub()
loggingTextBox.Text = "Operation completed!"
End Sub)
Finally
button1.Enabled = True
End Try
End Sub
Verwenden Sie für asynchrone Vorgänge, die im UI-Thread ausgeführt werden müssen, die asynchrone Überladung:
private async void button2_Click(object sender, EventArgs e)
{
button2.Enabled = false;
try
{
loggingTextBox.Text = "Starting operation...";
// Dispatch and run on a new thread, but wait for tasks to finish
// Exceptions are rethrown here, because await is used
await Task.WhenAll(Task.Run(SomeApiCallAsync),
Task.Run(SomeApiCallAsync),
Task.Run(SomeApiCallAsync));
// Dispatch and run on a new thread, but don't wait for task to finish
// Exceptions are not rethrown here, because await is not used
_ = Task.Run(SomeApiCallAsync);
}
catch (OperationCanceledException)
{
loggingTextBox.Text += "Operation canceled.";
}
catch (Exception ex)
{
loggingTextBox.Text += $"Error: {ex.Message}";
}
finally
{
button2.Enabled = true;
}
}
private async Task SomeApiCallAsync()
{
using var client = new HttpClient();
// Simulate random network delay
await Task.Delay(Random.Shared.Next(500, 2500));
// Do I/O asynchronously
string result = await client.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md");
// Marshal back to UI thread
await this.InvokeAsync(async (cancelToken) =>
{
loggingTextBox.Text += $"{Environment.NewLine}Operation finished at: {DateTime.Now:HH:mm:ss.fff}";
});
// Do more async I/O ...
}
Private Async Sub Button2_Click(sender As Object, e As EventArgs) Handles button2.Click
button2.Enabled = False
Try
loggingTextBox.Text = "Starting operation..."
' Dispatch and run on a new thread, but wait for tasks to finish
' Exceptions are rethrown here, because await is used
Await Task.WhenAll(Task.Run(AddressOf SomeApiCallAsync),
Task.Run(AddressOf SomeApiCallAsync),
Task.Run(AddressOf SomeApiCallAsync))
' Dispatch and run on a new thread, but don't wait for task to finish
' Exceptions are not rethrown here, because await is not used
Call Task.Run(AddressOf SomeApiCallAsync)
Catch ex As OperationCanceledException
loggingTextBox.Text += "Operation canceled."
Catch ex As Exception
loggingTextBox.Text += $"Error: {ex.Message}"
Finally
button2.Enabled = True
End Try
End Sub
Private Async Function SomeApiCallAsync() As Task
Using client As New HttpClient()
' Simulate random network delay
Await Task.Delay(Random.Shared.Next(500, 2500))
' Do I/O asynchronously
Dim result As String = Await client.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md")
' Marshal back to UI thread
' Extra work here in VB to handle ValueTask conversion
Await Me.InvokeAsync(DirectCast(
Async Function(cancelToken As CancellationToken) As Task
loggingTextBox.Text &= $"{Environment.NewLine}Operation finished at: {DateTime.Now:HH:mm:ss.fff}"
End Function,
Func(Of CancellationToken, Task)).AsValueTask() 'Extension method to convert Task
)
' Do more Async I/O ...
End Using
End Function
Hinweis
Wenn Sie Visual Basic verwenden, verwendet der vorherige Codeausschnitt eine Erweiterungsmethode, um eine ValueTask in ein Task. Der Code der Erweiterungsmethode ist auf GitHub verfügbar.
Beispiel: Verwenden der Control.Invoke-Methode
Im folgenden Beispiel wird ein Muster gezeigt, mit dem Sie threadsichere Aufrufe eines Windows Forms-Steuerelements sicherstellen können. Dabei wird die System.Windows.Forms.Control.InvokeRequired-Eigenschaft abgefragt, und die ID des Erstellungsthreads für das Steuerelement wird mit der ID des aufrufenden Threads verglichen. Wenn sie anders sind, sollten Sie die Control.Invoke Methode aufrufen.
Dies WriteTextSafe ermöglicht das Festlegen der Eigenschaft des TextBox Steuerelements Text auf einen neuen Wert. Die Methode fragt InvokeRequired ab. Wenn InvokeRequiredtrue zurückkehrt, ruft WriteTextSafe sich rekursiv selbst auf und übergibt sie als Delegat an die Invoke Methode. Wenn InvokeRequiredfalsezurückgibt, legt WriteTextSafe die TextBox.Text direkt fest. Der Button1_Click-Ereignishandler erstellt den neuen Thread und führt die WriteTextSafe-Methode aus.
private void button1_Click(object sender, EventArgs e)
{
WriteTextSafe("Writing message #1");
_ = Task.Run(() => WriteTextSafe("Writing message #2"));
}
public void WriteTextSafe(string text)
{
if (textBox1.InvokeRequired)
textBox1.Invoke(() => WriteTextSafe($"{text} (NON-UI THREAD)"));
else
textBox1.Text += $"{Environment.NewLine}{text}";
}
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
WriteTextSafe("Writing message #1")
Task.Run(Sub() WriteTextSafe("Writing message #2"))
End Sub
Private Sub WriteTextSafe(text As String)
If (TextBox1.InvokeRequired) Then
TextBox1.Invoke(Sub()
WriteTextSafe($"{text} (NON-UI THREAD)")
End Sub)
Else
TextBox1.Text += $"{Environment.NewLine}{text}"
End If
End Sub
Weitere Informationen dazu, wie Invoke sich die Unterschiede unterscheiden, finden Sie unter Grundlegendes InvokeAsynczum Unterschied: Invoke vs InvokeAsync.
Beispiel: Verwenden eines BackgroundWorkers
Eine einfache Möglichkeit zum Implementieren von Multithreadingszenarien, wobei sichergestellt wird, dass der Zugriff auf ein Steuerelement oder Formular nur im Hauptthread (UI-Thread) ausgeführt wird, ist mit der System.ComponentModel.BackgroundWorker Komponente, die ein ereignisgesteuertes Modell verwendet. Der Hintergrundthread löst das BackgroundWorker.DoWork Ereignis aus, das nicht mit dem Hauptthread interagiert. Der Hauptthread führt die Ereignishandler BackgroundWorker.ProgressChanged und BackgroundWorker.RunWorkerCompleted aus, die die Steuerelemente des Hauptthreads aufrufen können.
Von Bedeutung
Die BackgroundWorker Komponente ist nicht mehr der empfohlene Ansatz für asynchrone Szenarien in Windows Forms-Anwendungen. Während wir diese Komponente weiterhin aus Gründen der Abwärtskompatibilität unterstützen, wird nur das Entladen der Prozessorauslastung vom UI-Thread in einen anderen Thread behandelt. Es werden keine anderen asynchronen Szenarien wie Datei-E/A- oder Netzwerkvorgänge behandelt, bei denen der Prozessor möglicherweise nicht aktiv arbeitet.
Verwenden Sie async für die moderne asynchrone Programmierung stattdessen Methoden await . Wenn Sie prozessorintensive Arbeit explizit auslagern müssen, erstellen Task.Run und starten Sie eine neue Aufgabe, die Sie dann wie jeden anderen asynchronen Vorgang erwarten können. Weitere Informationen finden Sie unter Beispiel: Verwenden von Control.InvokeAsync (.NET 9 und höher) und Threadübergreifende Vorgänge und Ereignisse.
Behandeln Sie das BackgroundWorker-Ereignis, um einen threadsicheren Aufruf mit DoWork zu machen. Es gibt zwei Ereignisse, die der Hintergrundmitarbeiter zum Melden des Status verwendet: ProgressChanged und RunWorkerCompleted. Das ProgressChanged Ereignis wird verwendet, um Statusaktualisierungen mit dem Hauptthread zu kommunizieren, und das RunWorkerCompleted Ereignis wird verwendet, um zu signalisieren, dass der Hintergrundmitarbeiter abgeschlossen ist. Rufen Sie BackgroundWorker.RunWorkerAsyncauf, um den Hintergrundthread zu starten.
Das Beispiel zählt von 0 bis 10 im DoWork Ereignis, wobei nach jedem Zählen eine Sekunde pausiert wird. Er verwendet den ProgressChanged Ereignishandler, um die Nummer zurück an den Hauptthread zu melden und die TextBox Eigenschaft des Steuerelements Text festzulegen. Damit das ProgressChanged Ereignis funktioniert, muss die BackgroundWorker.WorkerReportsProgress Eigenschaft auf true gesetzt werden.
private void button1_Click(object sender, EventArgs e)
{
if (!backgroundWorker1.IsBusy)
backgroundWorker1.RunWorkerAsync(); // Not awaitable
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
int counter = 0;
int max = 10;
while (counter <= max)
{
backgroundWorker1.ReportProgress(0, counter.ToString());
System.Threading.Thread.Sleep(1000);
counter++;
}
}
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) =>
textBox1.Text = (string)e.UserState;
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
If (Not BackgroundWorker1.IsBusy) Then
BackgroundWorker1.RunWorkerAsync() ' Not awaitable
End If
End Sub
Private Sub BackgroundWorker1_DoWork(sender As Object, e As ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Dim counter = 0
Dim max = 10
While counter <= max
BackgroundWorker1.ReportProgress(0, counter.ToString())
System.Threading.Thread.Sleep(1000)
counter += 1
End While
End Sub
Private Sub BackgroundWorker1_ProgressChanged(sender As Object, e As ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
TextBox1.Text = e.UserState
End Sub
.NET Desktop feedback