如何:对 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. InvalidOperationException始终不安全的跨线程调用 Visual Studio 在调试期间,发生并且可能会出现在应用运行时。The InvalidOperationException always occurs for unsafe cross-thread calls during Visual Studio debugging, and may occur at app runtime. 可修复此问题,但你可以通过设置禁用异常Control.CheckForIllegalCrossThreadCalls属性设置为falseYou 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.

您可以生成并运行这些示例作为从.NET Framework 应用程序C#或 Visual Basic 命令行。You can build and run these examples as .NET Framework apps from the C# or Visual Basic command line. 有关详细信息,请参阅命令行上使用 csc.exe 生成中的命令行 (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方法查询InvokeRequiredThe WriteTextSafe method queries InvokeRequired. 如果InvokeRequired将返回trueWriteTextSafe传递SafeCallDelegateInvoke进行实际调用控件的方法。If InvokeRequired returns true, WriteTextSafe passes the SafeCallDelegate to the Invoke method to make the actual call to the control. 如果InvokeRequired将返回falseWriteTextSafe设置TextBox.Text直接。If 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