Freigeben über


Behandeln von Threadübergreifenden Vorgängen mit Steuerelementen

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)

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 Task Wert 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