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 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:
- Die System.Windows.Forms.Control.Invoke-Methode, die einen Delegaten aus dem Hauptthread aufruft, um das Steuerelement aufzurufen
- 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 InvokeRequiredfalse
zurü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
.NET Desktop feedback