多线程处理可以改进 Windows 窗体应用的性能,但对 Windows 窗体控件的访问本质上不是线程安全的。 多线程处理可将代码公开到严重和复杂的 bug。 有两个或两个以上线程操作控件可能会迫使该控件处于不一致状态并导致争用条件、死锁和冻结或挂起。 如果要在应用中实现多线程处理,请务必以线程安全的方式调用跨线程控件。?
可通过两种方法从未创建 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.";
Visual Studio 调试器通过引发?InvalidOperationException?检测这些不安全线程调用,并显示消息“跨线程操作无效。控件从创建它的线程以外的线程访问。”在 Visual Studio 调试期间,对于不安全的跨线程调用总是会发生?InvalidOperationException,并且可能在应用运行时发生。 应解决此问题,但也可以通过将?Control.CheckForIllegalCrossThreadCalls?属性设置为?false
?来禁用该异常。
以下代码示例演示了两种从未创建 Windows 窗体控件的线程安全调用该窗体的方法:
在这两个示例中,后台线程都会休眠一秒钟以模拟该线程中正在完成的工作。
下面的示例演示了一种用于确保对 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;
}
实现多线程处理的一种简单方法是使用?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;