如何:对 Windows 窗体控件进行线程安全调用How to: Make thread-safe calls to Windows Forms controls

多线程处理可以提高 Windows 窗体应用的性能,但对 Windows 窗体控件的访问本质上不是线程安全的。Multithreading can improve the performance of Windows Forms apps, but access to Windows Forms controls isn't inherently thread-safe. 多线程处理可以将你的代码公开给非常严重且复杂的 bug。Multithreading can expose your code to very serious and complex bugs. 处理控件的两个或多个线程可能会强制控件处于不一致状态并导致争用情况、死锁和冻结或挂起。Two or more threads manipulating a control can force the control into an inconsistent state and lead to race conditions, deadlocks, and freezes or hangs. 如果在应用程序中实现多线程处理,请确保以线程安全的方式调用跨线程控件。If you implement multithreading in your app, be sure to call cross-thread controls in a thread-safe way. 有关详细信息,请参阅托管线程处理最佳做法For more information, see Managed threading best practices.

可以通过两种方法从未创建该控件的线程安全调用 Windows 窗体控件。There are two ways to safely call a Windows Forms control from a thread that didn't create that control. 您可以使用 System.Windows.Forms.Control.Invoke 方法调用在主线程中创建的委托,该委托又调用控件。You can use the System.Windows.Forms.Control.Invoke method to call a delegate created in the main thread, which in turn calls the control. 或者,您可以实现一个 System.ComponentModel.BackgroundWorker,该使用事件驱动模型将后台线程中完成的工作与报表上的结果进行分隔。Or, you can implement a System.ComponentModel.BackgroundWorker, which uses an event-driven model to separate work done in the background thread from reporting on the results.

不安全的跨线程调用Unsafe cross-thread calls

直接从未创建控件的线程调用控件是不安全的。It's unsafe to call a control directly from a thread that didn't create it. 下面的代码段演示对 System.Windows.Forms.TextBox 控件的不安全调用。The following code snippet illustrates an unsafe call to the System.Windows.Forms.TextBox control. Button1_Click 事件处理程序创建一个新的 WriteTextUnsafe 线程,该线程直接设置主线程的 TextBox.Text 属性。The Button1_Click event handler creates a new WriteTextUnsafe thread, which sets the main thread's TextBox.Text property directly.

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 来检测这些不安全线程调用,跨线程操作无效。控件 "" 从创建它的线程之外的其他线程访问。The Visual Studio debugger detects these unsafe thread calls by raising an InvalidOperationException with the message, Cross-thread operation not valid. Control "" accessed from a thread other than the thread it was created on. 在 Visual Studio 调试过程中,InvalidOperationException 始终对不安全的跨线程调用发生,并且可能会在应用运行时发生。The InvalidOperationException always occurs for unsafe cross-thread calls during Visual Studio debugging, and may occur at app runtime. 应修复此问题,但可以通过将 Control.CheckForIllegalCrossThreadCalls 属性设置为 false来禁用异常。You should fix the issue, but you can disable the exception by setting the Control.CheckForIllegalCrossThreadCalls property to false.

安全的跨线程调用Safe cross-thread calls

下面的代码示例演示了两种从未创建 Windows 窗体控件的线程安全调用的方法:The following code examples demonstrate two ways to safely call a Windows Forms control from a thread that didn't create it:

  1. System.Windows.Forms.Control.Invoke 方法,该方法从主线程调用委托以调用控件。The System.Windows.Forms.Control.Invoke method, which calls a delegate from the main thread to call the control.
  2. 一个 System.ComponentModel.BackgroundWorker 组件,该组件提供事件驱动模型。A System.ComponentModel.BackgroundWorker component, which offers an event-driven model.

在这两个示例中,后台线程休眠一秒钟,以模拟在该线程中完成的工作。In both examples, the background thread sleeps for one second to simulate work being done in that thread.

你可以从C#或 Visual Basic 命令行 .NET Framework 应用生成并运行这些示例。You can build and run these examples as .NET Framework apps from the C# or Visual Basic command line. 有关详细信息,请参阅通过 csc 生成命令行从命令行生成(Visual Basic)For more information, see Command-line building with csc.exe or Build from the command line (Visual Basic).

从.NET Core 3.0 开始,你还可以生成并运行示例作为 Windows.NET Core 应用程序从具有.NET Core Windows 窗体的文件夹 <文件夹名称>.csproj 项目文件。Starting with .NET Core 3.0, you can also build and run the examples as Windows .NET Core apps from a folder that has a .NET Core Windows Forms <folder name>.csproj project file.

示例:对委托使用 Invoke 方法Example: Use the Invoke method with a delegate

下面的示例演示了一种用于确保对 Windows 窗体控件进行线程安全调用的模式。The following example demonstrates a pattern for ensuring thread-safe calls to a Windows Forms control. 它将查询 System.Windows.Forms.Control.InvokeRequired 属性,该属性将控件的创建线程 ID 与调用线程 ID 进行比较。It queries the System.Windows.Forms.Control.InvokeRequired property, which compares the control's creating thread ID to the calling thread ID. 如果线程 Id 相同,则它会直接调用控件。If the thread IDs are the same, it calls the control directly. 如果线程 Id 不同,它将使用主线程中的委托调用 Control.Invoke 方法,这将对控件进行实际调用。If the thread IDs are different, it calls the Control.Invoke method with a delegate from the main thread, which makes the actual call to the control.

SafeCallDelegate 允许设置 TextBox 控件的 Text 属性。The SafeCallDelegate enables setting the TextBox control's Text property. WriteTextSafe 方法 InvokeRequired查询。The WriteTextSafe method queries InvokeRequired. 如果 InvokeRequired 返回 trueWriteTextSafe 会将 SafeCallDelegate 传递到 Invoke 方法,以对控件进行实际调用。If InvokeRequired returns true, WriteTextSafe passes the SafeCallDelegate to the Invoke method to make the actual call to the control. 如果 InvokeRequired 返回 falseWriteTextSafe 将直接设置 TextBox.TextIf InvokeRequired returns false, WriteTextSafe sets the TextBox.Text directly. Button1_Click 事件处理程序将创建新线程并运行 WriteTextSafe 方法。The Button1_Click event handler creates the new thread and runs the WriteTextSafe method.

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 事件处理程序Example: Use a BackgroundWorker event handler

实现多线程处理的一种简单方法是使用 System.ComponentModel.BackgroundWorker 组件,该组件使用事件驱动模型。An easy way to implement multithreading is with the System.ComponentModel.BackgroundWorker component, which uses an event-driven model. 后台线程运行不与主线程交互的 BackgroundWorker.DoWork 事件。The background thread runs the BackgroundWorker.DoWork event, which doesn't interact with the main thread. 主线程运行 BackgroundWorker.ProgressChangedBackgroundWorker.RunWorkerCompleted 事件处理程序,这些处理程序可调用主线程的控件。The main thread runs the BackgroundWorker.ProgressChanged and BackgroundWorker.RunWorkerCompleted event handlers, which can call the main thread's controls.

若要通过使用 BackgroundWorker进行线程安全调用,请在后台线程中创建一个方法来完成工作,并将其绑定到 DoWork 事件。To make a thread-safe call by using BackgroundWorker, create a method in the background thread to do the work, and bind it to the DoWork event. 在主线程中创建另一个方法来报告后台工作的结果,并将其绑定到 ProgressChangedRunWorkerCompleted 事件。Create another method in the main thread to report the results of the background work, and bind it to the ProgressChanged or RunWorkerCompleted event. 若要启动后台线程,请调用 BackgroundWorker.RunWorkerAsyncTo start the background thread, call BackgroundWorker.RunWorkerAsync.

该示例使用 RunWorkerCompleted 事件处理程序来设置 TextBox 控件的 Text 属性。The example uses the RunWorkerCompleted event handler to set the TextBox control's Text property. 有关使用 ProgressChanged 事件的示例,请参阅 BackgroundWorkerFor an example using the ProgressChanged event, see 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

另请参阅See also