Threadsicheres Aufrufen von Steuerelementen (Windows Forms .NET)

Multithreading kann die Leistung von Windows Forms-Apps verbessern, der Zugriff auf Windows Forms-Steuerelemente ist jedoch nicht inhärent threadsicher. Multithreading kann zu schwerwiegenden und komplexen Fehlern in Ihrem Code führen. Wenn zwei oder mehr Threads ein Steuerelement ändern, kann das Steuerelement einen inkonsistenten Zustand annehmen, und es kann zu Racebedingungen, Deadlocks und zum Einfrieren kommen. Wenn Sie Multithreading in Ihrer App implementieren, sollten Sie threadübergreifende Steuerelemente auf threadsichere Weise aufrufen. Weitere Informationen finden Sie unter Empfohlene Vorgehensweise für das verwaltete Threading.

Wichtig

Der Desktopleitfaden zu .NET 7 und .NET 6 ist in Bearbeitung.

Es gibt zwei Möglichkeiten zum sicheren Aufrufen eines Windows Forms-Steuerelements aus einem Thread, der dieses Steuerelement nicht erstellt hat. Verwenden Sie die System.Windows.Forms.Control.Invoke-Methode, um einen Delegaten aufzurufen, der im Hauptthread erstellt wurde, wodurch wiederum das Steuerelement aufgerufen wird. Oder implementieren Sie einen System.ComponentModel.BackgroundWorker, der ein ereignisgesteuertes Modell verwendet, um die im Hintergrundthread ausgeführten Vorgänge von der Berichterstattung über die Ergebnisse trennt.

Unsichere threadübergreifende Aufrufe

Es gilt als 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 button1_Click(object sender, EventArgs e)
{
    var thread2 = new System.Threading.Thread(WriteTextUnsafe);
    thread2.Start();
}

private void WriteTextUnsafe() =>
    textBox1.Text = "This text was set unsafely.";
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    Dim thread2 As New System.Threading.Thread(AddressOf WriteTextUnsafe)
    thread2.Start()
End Sub

Private Sub WriteTextUnsafe()
    TextBox1.Text = "This text was set unsafely."
End Sub

Der Visual Studio-Debugger erkennt diese unsicheren Threadaufrufe, indem eine InvalidOperationException mit der Meldung ausgelöst wird, dass der threadübergreifende Vorgang ungültig ist und dass auf das Steuerelement von einem anderen Thread als dem Thread zugegriffen wurde, von dem es erstellt wurde. Die InvalidOperationException tritt immer bei unsicheren Threadaufrufen während des Visual Studio Debuggings auf und kann auch zur App-Laufzeit auftreten. Sie sollten das Problem beheben. Sie können die Ausnahme aber auch deaktivieren, indem Sie die Control.CheckForIllegalCrossThreadCalls-Eigenschaft auf false festlegen.

Sichere threadübergreifende Aufrufe

Die folgenden Codebeispiele veranschaulichen zwei Möglichkeiten zum sicheren Aufrufen eines Windows Forms-Steuerelements aus einem Thread, der es nicht erstellt hat:

  1. Die System.Windows.Forms.Control.Invoke-Methode ruft einen Delegaten aus dem Hauptthread auf, um das Steuerelement aufzurufen.
  2. Eine System.ComponentModel.BackgroundWorker-Komponente bietet ein ereignisgesteuertes Modell.

In beiden Beispielen wartet der Hintergrundthread eine Sekunde, um die in diesem Thread ausgeführten Vorgänge zu simulieren.

Beispiel: Verwenden der 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 sich unterscheiden, sollten Sie die Control.Invoke-Methode aufrufen.

Mit WriteTextSafe können Sie die Einstellung der Text-Eigenschaft des TextBox-Steuerelements auf einen neuen Wert festlegen. Die Methode fragt InvokeRequired ab. Wenn InvokeRequiredtrue zurückgibt, ruft WriteTextSafe rekursiv sich selbst auf und übergibt die Methode als Delegaten an die Invoke-Methode. Wenn InvokeRequiredfalse zurückgibt, legt WriteTextSafe den 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)
{
    var threadParameters = new System.Threading.ThreadStart(delegate { WriteTextSafe("This text was set safely."); });
    var thread2 = new System.Threading.Thread(threadParameters);
    thread2.Start();
}

public void WriteTextSafe(string text)
{
    if (textBox1.InvokeRequired)
    {
        // Call this same method but append THREAD2 to the text
        Action safeWrite = delegate { WriteTextSafe($"{text} (THREAD2)"); };
        textBox1.Invoke(safeWrite);
    }
    else
        textBox1.Text = text;
}
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

    Dim threadParameters As New System.Threading.ThreadStart(Sub()
                                                                 WriteTextSafe("This text was set safely.")
                                                             End Sub)

    Dim thread2 As New System.Threading.Thread(threadParameters)
    thread2.Start()

End Sub

Private Sub WriteTextSafe(text As String)

    If (TextBox1.InvokeRequired) Then

        TextBox1.Invoke(Sub()
                            WriteTextSafe($"{text} (THREAD2)")
                        End Sub)

    Else
        TextBox1.Text = text
    End If

End Sub

Beispiel: Verwenden eines BackgroundWorker

Eine einfache Möglichkeit zum Implementieren von Multithreading bietet die 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.

Behandeln Sie das DoWork-Ereignis, um einen threadsicheren Aufruf mithilfe von BackgroundWorker durchzuführen. Der Hintergrundworker verwendet zwei Ereignisse, um den Status zu melden: ProgressChanged und RunWorkerCompleted. Über das ProgressChanged-Ereignis werden Statusänderungen am Hauptthread gemeldet, und mit dem RunWorkerCompleted-Ereignis wird signalisiert, dass der Hintergrundworker seine Arbeit abgeschlossen hat. Rufen Sie BackgroundWorker.RunWorkerAsync auf, um den Hintergrundthread zu starten.

Das Beispiel zählt im DoWork-Ereignis von 0 bis 10 mit jeweils einer Pause von einer Sekunde. Mithilfe des ProgressChanged-Ereignishandlers wird die Nummer zurück an den Hauptthread gemeldet und die Text-Eigenschaft des TextBox-Steuerelements festgelegt. Damit das ProgressChanged-Ereignis funktioniert, muss die BackgroundWorker.WorkerReportsProgress-Eigenschaft auf true festgelegt werden.

private void button1_Click(object sender, EventArgs e)
{
    if (!backgroundWorker1.IsBusy)
        backgroundWorker1.RunWorkerAsync();
}

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()
    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