Anleitung: Threadsicheres Aufrufen von Windows Forms-Steuerelementen

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, wird das Steuerelement möglicherweise in einen inkonsistenten Zustand versetzt, 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 Best Practices für verwaltetes Threading.

Es gibt zwei Möglichkeiten zum sicheren Aufrufen eines Windows Forms-Steuerelements aus einem Thread, der dieses Steuerelement nicht erstellt hat. Sie können die System.Windows.Forms.Control.Invoke-Methode verwenden, um einen Delegaten aufzurufen, der im Hauptthread erstellt wurde, und durch den wiederum das Steuerelement aufgerufen wird. Alternativ können Sie einen Hintergrundworker vom Typ System.ComponentModel.BackgroundWorker implementieren, der ein ereignisgesteuertes Modell verwendet, um die im Hintergrundthread ausgeführten Vorgänge von der Berichterstellung für die Ergebnisse zu trennen.

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)
{
    thread2 = new Thread(new ThreadStart(WriteTextUnsafe));
    thread2.Start();
}
private void WriteTextUnsafe()
{
    textBox1.Text = "This text was set unsafely.";
}
Private Sub Button1_Click(ByVal sender As Object, e As EventArgs) Handles Button1.Click
    Thread2 = New Thread(New ThreadStart(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. Hierzu wird eine Ausnahme vom Typ InvalidOperationException mit folgender Meldung ausgelöst: Ungültiger threadübergreifender Vorgang: Der Zugriff auf das Steuerelement XY erfolgte von einem anderen Thread als dem Thread, für den es erstellt wurde. Die Ausnahme vom Typ 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.

Sie können diese Beispiele als .NET Framework-Anwendungen in der C#- oder Visual Basic-Befehlszeile erstellen und ausführen. Weitere Informationen finden Sie unter C#-Compileroptionen oder unter Erstellen von der Befehlszeile aus (Visual Basic).

Ab .NET Core 3.0 können Sie die Beispiele auch als Windows .NET Core-Anwendungen in einem Ordner erstellen und ausführen, in dem eine .NET Core Windows Forms-Projektdatei namens <Ordnername>.csproj vorhanden ist.

Beispiel: Verwenden der Invoke-Methode mit einem Delegaten

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 die Thread-IDs identisch sind, wird das Steuerelement direkt aufgerufen. Wenn sich die Thread-IDs unterscheiden, wird die Control.Invoke-Methode mit einem Delegaten aus dem Hauptthread aufgerufen, der dann den eigentlichen Aufruf für das Steuerelement ausführt.

Mit SafeCallDelegate können Sie die Einstellung der Text-Eigenschaft des TextBox-Steuerelements festlegen. Die WriteTextSafe-Methode fragt InvokeRequired ab. Falls InvokeRequired den Wert true zurückgibt, übergibt WriteTextSafe den Delegaten vom Typ SafeCallDelegate an die Invoke-Methode, um den eigentlichen Aufruf für das Steuerelement auszuführen. Wenn InvokeRequired den Wert false zurückgibt, wird TextBox.Text von WriteTextSafe direkt festgelegt. Der Button1_Click-Ereignishandler erstellt den neuen Thread und führt die WriteTextSafe-Methode aus.

using System;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;

public class InvokeThreadSafeForm : Form
{
    private delegate void SafeCallDelegate(string text);
    private Button button1;
    private TextBox textBox1;
    private Thread thread2 = null;

    [STAThread]
    static void Main()
    {
        Application.SetCompatibleTextRenderingDefault(false);
        Application.EnableVisualStyles();
        Application.Run(new InvokeThreadSafeForm());
    }
    public InvokeThreadSafeForm()
    {
        button1 = new Button
        {
            Location = new Point(15, 55),
            Size = new Size(240, 20),
            Text = "Set text safely"
        };
        button1.Click += new EventHandler(Button1_Click);
        textBox1 = new TextBox
        {
            Location = new Point(15, 15),
            Size = new Size(240, 20)
        };
        Controls.Add(button1);
        Controls.Add(textBox1);
    }

    private void Button1_Click(object sender, EventArgs e)
    {
        thread2 = new Thread(new ThreadStart(SetText));
        thread2.Start();
        Thread.Sleep(1000);
    }

    private void WriteTextSafe(string text)
    {
        if (textBox1.InvokeRequired)
        {
            var d = new SafeCallDelegate(WriteTextSafe);
            textBox1.Invoke(d, new object[] { text });
        }
        else
        {
            textBox1.Text = text;
        }
    }

    private void SetText()
    {
        WriteTextSafe("This text was set safely.");
    }
}
Imports System.Drawing
Imports System.Threading
Imports System.Windows.Forms

Public Class InvokeThreadSafeForm : Inherits Form

    Public Shared Sub Main()
        Application.SetCompatibleTextRenderingDefault(False)
        Application.EnableVisualStyles()
        Dim frm As New InvokeThreadSafeForm()
        Application.Run(frm)
    End Sub

    Dim WithEvents Button1 As Button
    Dim TextBox1 As TextBox
    Dim Thread2 as Thread = Nothing

    Delegate Sub SafeCallDelegate(text As String)

    Private Sub New()
        Button1 = New Button()
        With Button1
            .Location = New Point(15, 55)
            .Size = New Size(240, 20)
            .Text = "Set text safely"
        End With
        TextBox1 = New TextBox()
        With TextBox1
            .Location = New Point(15, 15)
            .Size = New Size(240, 20)
        End With
        Controls.Add(Button1)
        Controls.Add(TextBox1)
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Thread2 = New Thread(New ThreadStart(AddressOf SetText))
        Thread2.Start()
        Thread.Sleep(1000)
    End Sub

    Private Sub WriteTextSafe(text As String)
        If TextBox1.InvokeRequired Then
            Dim d As New SafeCallDelegate(AddressOf SetText)
            TextBox1.Invoke(d, New Object() {text})
        Else
            TextBox1.Text = text
        End If
    End Sub

    Private Sub SetText()
        WriteTextSafe("This text was set safely.")
    End Sub
End Class

Beispiel: Verwenden eines BackgroundWorker-Ereignishandlers

Eine einfache Möglichkeit zum Implementieren von Multithreading bietet die System.ComponentModel.BackgroundWorker-Komponente, die ein ereignisgesteuertes Modell verwendet. Der Hintergrundthread führt 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.

Wenn Sie einen threadsicheren Aufruf mithilfe von BackgroundWorker erstellen möchten, erstellen Sie im Hintergrundthread eine Methode für die gewünschte Aufgabe, und binden Sie sie an das DoWork-Ereignis. Erstellen Sie eine weitere Methode im Hauptthread, um die Ergebnisse der Hintergrundarbeit zu melden, und binden Sie sie an das Ereignis ProgressChanged oder RunWorkerCompleted. Rufen Sie BackgroundWorker.RunWorkerAsync auf, um den Hintergrundthread zu starten.

Im Beispiel wird der RunWorkerCompleted-Ereignishandler verwendet, um die Text-Eigenschaft des TextBox-Steuerelements festzulegen. Ein Beispiel für die Verwendung des ProgressChanged-Ereignisses finden Sie unter BackgroundWorker.

using System;
using System.ComponentModel;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;

public class BackgroundWorkerForm : Form
{
    private BackgroundWorker backgroundWorker1;
    private Button button1;
    private TextBox textBox1;

    [STAThread]
    static void Main()
    {
        Application.SetCompatibleTextRenderingDefault(false);
        Application.EnableVisualStyles();
        Application.Run(new BackgroundWorkerForm());
    }
    public BackgroundWorkerForm()
    {
        backgroundWorker1 = new BackgroundWorker();
        backgroundWorker1.DoWork += new DoWorkEventHandler(BackgroundWorker1_DoWork);
        backgroundWorker1.RunWorkerCompleted += new RunWorkerCompletedEventHandler(BackgroundWorker1_RunWorkerCompleted);
        button1 = new Button
        {
            Location = new Point(15, 55),
            Size = new Size(240, 20),
            Text = "Set text safely with BackgroundWorker"
        };
        button1.Click += new EventHandler(Button1_Click);
        textBox1 = new TextBox
        {
            Location = new Point(15, 15),
            Size = new Size(240, 20)
        };
        Controls.Add(button1);
        Controls.Add(textBox1);
    }
    private void Button1_Click(object sender, EventArgs e)
    {
        backgroundWorker1.RunWorkerAsync();
    }

    private void BackgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        // Sleep 2 seconds to emulate getting data.
        Thread.Sleep(2000);
        e.Result = "This text was set safely by BackgroundWorker.";
    }

    private void BackgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        textBox1.Text = e.Result.ToString();
    }
}
Imports System.ComponentModel
Imports System.Drawing
Imports System.Threading
Imports System.Windows.Forms

Public Class BackgroundWorkerForm : Inherits Form

    Public Shared Sub Main()
        Application.SetCompatibleTextRenderingDefault(False)
        Application.EnableVisualStyles()
        Dim frm As New BackgroundWorkerForm()
        Application.Run(frm)
    End Sub

    Dim WithEvents BackgroundWorker1 As BackgroundWorker
    Dim WithEvents Button1 As Button
    Dim TextBox1 As TextBox

    Private Sub New()
        BackgroundWorker1 = New BackgroundWorker()
        Button1 = New Button()
        With Button1
            .Text = "Set text safely with BackgroundWorker"
            .Location = New Point(15, 55)
            .Size = New Size(240, 20)
        End With
        TextBox1 = New TextBox()
        With TextBox1
            .Location = New Point(15, 15)
            .Size = New Size(240, 20)
        End With
        Controls.Add(Button1)
        Controls.Add(TextBox1)
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        BackgroundWorker1.RunWorkerAsync()
    End Sub

    Private Sub BackgroundWorker1_DoWork(sender As Object, e As DoWorkEventArgs) _
     Handles BackgroundWorker1.DoWork
        ' Sleep 2 seconds to emulate getting data.
        Thread.Sleep(2000)
        e.Result = "This text was set safely by BackgroundWorker."
    End Sub

    Private Sub BackgroundWorker1_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs) _
     Handles BackgroundWorker1.RunWorkerCompleted
        textBox1.Text = e.Result.ToString()
    End Sub
End Class

Weitere Informationen