Freigeben über


So machen Sie threadsichere Aufrufe an Windows Forms-Steuerelemente

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 komplexen und ernsten 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. Sie können die System.Windows.Forms.Control.Invoke-Methode verwenden, um einen im Hauptthread erstellten Delegat aufzurufen, der wiederum das Steuerelement aufruft. Alternativ können Sie einen 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 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 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. 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, aber Sie können die Ausnahme deaktivieren, indem Sie die eigenschaft Control.CheckForIllegalCrossThreadCalls auf falsefestlegen.

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, die einen Delegaten aus dem Hauptthread aufruft, um das Steuerelement aufzurufen
  2. Eine System.ComponentModel.BackgroundWorker-Komponente, die ein ereignisgesteuertes Modell bietet.

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-Apps über die Befehlszeile C# oder Visual Basic erstellen und ausführen. Weitere Informationen finden Sie unter Befehlszeilenerstellung mit csc.exe oder unter Erstellen von der Befehlszeile aus (Visual Basic).

Ab .NET Core 3.0 können Sie die Beispiele auch als Windows .NET Core-Apps aus einem Ordner erstellen und ausführen, in dem ein .NET Core Windows Forms <Ordnername>.csproj Projektdatei enthalten 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 Delegat 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 SafeCallDelegate an die Invoke-Methode, um den eigentlichen Aufruf für das Steuerelement auszuführen. 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.

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 ist 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.RunWorkerAsyncauf, 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