【WinForm.NET开发】如何对控件进行线程安全的调用

本文内容

  1. 不安全的跨线程调用
  2. 安全的跨线程调用
  3. 示例:使用 Invoke 方法
  4. 示例:使用 BackgroundWorker

多线程处理可以改进 Windows 窗体应用的性能,但对 Windows 窗体控件的访问本质上不是线程安全的。 多线程处理可将代码公开到严重和复杂的 bug。 有两个或两个以上线程操作控件可能会迫使该控件处于不一致状态并导致争用条件、死锁和冻结或挂起。 如果要在应用中实现多线程处理,请务必以线程安全的方式调用跨线程控件。 

可通过两种方法从未创建 Windows 窗体控件的线程安全地调用该控件。 使用 System.Windows.Forms.Control.Invoke 方法调用在主线程中创建的委托,进而调用控件。 或者,实现一个 System.ComponentModel.BackgroundWorker,它使用事件驱动模型将后台线程中完成的工作与结果报告分开。

1、不安全的跨线程调用

直接从未创建控件的线程调用该控件是不安全的。 以下代码片段演示了对 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 来禁用该异常。

2、安全的跨线程调用

以下代码示例演示了两种从未创建 Windows 窗体控件的线程安全调用该窗体的方法:

  1. System.Windows.Forms.Control.Invoke 方法,它从主线程调用委托以调用控件。
  2. System.ComponentModel.BackgroundWorker 组件,它提供事件驱动模型。

在这两个示例中,后台线程都会休眠一秒钟以模拟该线程中正在完成的工作。

3、示例:使用 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;
}

4、示例:使用 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;

你可能感兴趣的:(WinForm.NET,专栏,.net,安全,windows,c#)