如何对 Windows 窗体控件进行线程安全调用
多线程可以提高 Windows 窗体应用的性能,但对 Windows 窗体控件的访问本质上不是线程安全的。 多线程可能会使您的代码受到非常严重和复杂的错误的影响。 有两个或两个以上线程操作控件可能会迫使该控件处于不一致状态并导致争用条件、死锁和冻结或挂起。 如果在应用中实现多线程处理,请务必以线程安全的方式调用跨线程控件。 有关详细信息,请参阅 管理线程最佳实践。
有两种方法可以从没有创建该控件的线程中安全地调用 Windows 窗体控件。 可以使用 System.Windows.Forms.Control.Invoke 方法调用在主线程中创建的委托,进而调用该控件。 或者,可以实现 System.ComponentModel.BackgroundWorker,该模型使用事件驱动模型将后台线程中完成的工作与报告结果分开。
不安全的跨线程调用
直接从未创建控件的线程调用该控件是不安全的。 以下代码片段演示了对 System.Windows.Forms.TextBox 控件的不安全调用。 Button1_Click
事件处理程序创建一个新的 WriteTextUnsafe
线程,该线程直接设置主线程的 TextBox.Text 属性。
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
Visual Studio 调试器通过引发 InvalidOperationException 检测这些不安全线程调用,并显示消息:跨线程操作无效。控件 "" 从创建它的线程以外的线程访问。在 Visual Studio 调试期间,对于不安全的跨线程调用总是会发生 InvalidOperationException,并且可能在应用运行时发生。 应解决此问题,但可以通过将 Control.CheckForIllegalCrossThreadCalls 属性设置为 false
来禁用异常。
安全的跨线程调用
以下代码示例演示了两种从未创建 Windows 窗体控件的线程安全调用该窗体的方法:
- System.Windows.Forms.Control.Invoke 方法,它从主线程调用委托以调用控件。
- 提供事件驱动模型的 System.ComponentModel.BackgroundWorker 组件。
在这两个示例中,后台线程休眠 1 秒,以模拟在该线程中完成的工作。
可以从 C# 或 Visual Basic 命令行生成并运行这些示例作为 .NET Framework 应用。 有关详细信息,请参阅在命令行上使用 csc.exe 生成或从命令行生成 (Visual Basic)。
从 .NET Core 3.0 开始,还可以从具有 .NET Core Windows 窗体 <文件夹名称>.csproj 项目文件的文件夹生成并运行示例作为 Windows .NET Core 应用。
示例:将 Invoke 方法与委托配合使用
以下示例演示了一种模式,用于确保对 Windows 窗体控件的线程安全调用。 它查询 System.Windows.Forms.Control.InvokeRequired 属性,该属性将控件的创建线程 ID 与调用线程 ID 进行比较。 如果线程 ID 相同,则直接调用控件。 如果线程 ID 不同,它会通过主线程的委托来调用 Control.Invoke 方法,并进行对控件的实际调用。
SafeCallDelegate
允许设置 TextBox 控件的 Text 属性。 WriteTextSafe
方法查询 InvokeRequired。 如果 InvokeRequired 返回 true
,WriteTextSafe
将 SafeCallDelegate
传递给 Invoke 方法,以便对控件进行实际调用。 如果 InvokeRequired 返回 false
,WriteTextSafe
直接设置 TextBox.Text。 Button1_Click
事件处理程序将创建新线程并运行 WriteTextSafe
方法。
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
示例:使用 BackgroundWorker 事件处理程序
实现多线程的一种简单方法是使用 System.ComponentModel.BackgroundWorker 组件,该组件使用事件驱动模型。 后台线程运行 BackgroundWorker.DoWork 事件,该事件不与主线程交互。 主线程运行 BackgroundWorker.ProgressChanged 和 BackgroundWorker.RunWorkerCompleted 事件处理程序,后者可以调用主线程的控件。
若要使用 BackgroundWorker进行线程安全调用,请在后台线程中创建一个执行工作的方法,并将其绑定到 DoWork 事件。 在主线程中创建另一种方法来报告后台工作的结果,并将其绑定到 ProgressChanged 或 RunWorkerCompleted 事件。 若要启动后台线程,请调用 BackgroundWorker.RunWorkerAsync。
该示例使用 RunWorkerCompleted 事件处理程序设置 TextBox 控件的 Text 属性。 有关使用 ProgressChanged 事件的示例,请参阅 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
另请参阅
- BackgroundWorker
- 指南:在后台运行操作
- 如何:实现使用后台操作的窗体
- 使用 .NET Framework 开发自定义 Windows 窗体控件