多线程可以提高 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)
{
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
Visual Studio 调试器通过引发InvalidOperationException消息来检测这些不安全的线程调用,消息为跨线程操作无效。从创建线程以外的线程访问控件。 在 Visual Studio 调试期间,不安全的跨线程调用始终会发生,并且可能在应用程序运行时发生。 应解决此问题,但可以通过将 Control.CheckForIllegalCrossThreadCalls 属性设置为 false
来禁用异常。
安全的跨线程调用
以下代码示例演示了两种从未创建 Windows 窗体控件的线程安全调用该窗体的方法:
- System.Windows.Forms.Control.Invoke 方法,它从主线程调用委托以调用控件。
- 提供事件驱动模型的 System.ComponentModel.BackgroundWorker 组件。
在这两个示例中,后台线程休眠 1 秒,以模拟在该线程中完成的工作。
示例:使用 Invoke 方法
以下示例演示了一种模式,用于确保对 Windows 窗体控件的线程安全调用。 它查询 System.Windows.Forms.Control.InvokeRequired 属性,该属性将控件的创建线程 ID 与调用线程 ID 进行比较。 如果它们不同,则应调用该方法 Control.Invoke 。
启用 WriteTextSafe
将 TextBox 控件 Text 的属性设置为新值。 方法查询 InvokeRequired。 如果 InvokeRequired 返回 true
, WriteTextSafe
则以递归方式调用自身,将该方法作为委托传递给 Invoke 该方法。 如果 InvokeRequired 返回 false
,WriteTextSafe
直接设置 TextBox.Text。
Button1_Click
事件处理程序将创建新线程并运行 WriteTextSafe
方法。
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
示例:使用 BackgroundWorker
实现多线程的一种简单方法是使用 System.ComponentModel.BackgroundWorker 组件,该组件使用事件驱动模型。 后台线程引发 BackgroundWorker.DoWork 事件,该事件不会与主线程交互。 主线程运行 BackgroundWorker.ProgressChanged 和 BackgroundWorker.RunWorkerCompleted 事件处理程序,后者可以调用主线程的控件。
若要通过使用 BackgroundWorker 进行线程安全的调用,请处理 DoWork 事件。 后台处理程序用于报告状态的事件有两个: ProgressChanged 和 RunWorkerCompleted。 该 ProgressChanged
事件用于将状态更新传达给主线程,该 RunWorkerCompleted
事件用于指示后台辅助角色已完成其工作。 若要启动后台线程,请调用 BackgroundWorker.RunWorkerAsync。
该示例在 DoWork
事件中从 0 计数到 10,在每次计数之间暂停一秒。 它使用 ProgressChanged 事件处理程序将数字报告回主线程,并设置 TextBox 控件 Text 的属性。 若要使 ProgressChanged 事件正常工作,属性 BackgroundWorker.WorkerReportsProgress 必须设置为 true
。
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