如何:對 Windows Forms 控制項進行安全線程呼叫How to: Make thread-safe calls to Windows Forms controls

多執行緒可以改善 Windows Forms 應用程式的效能,但 Windows Forms 控制項的存取原本就不是安全線程。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. 如需詳細資訊,請參閱Managed 執行緒最佳做法For more information, see Managed threading best practices.

有兩種方式可以從未建立該控制項的執行緒安全地呼叫 Windows Forms 控制項。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 Forms 控制項的方式: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. 如需詳細資訊,請參閱使用 ngen.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 開始,您也可以從具有 .NET Core Windows Forms 的資料夾中,建立並執行範例做為 Windows .NET Core 應用程式 <資料夾名稱 > .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 Forms 控制項的安全線程呼叫。The following example demonstrates a pattern for ensuring thread-safe calls to a Windows Forms control. 它會查詢 System.Windows.Forms.Control.InvokeRequired 屬性,這會將控制項的建立執行緒識別碼與呼叫執行緒識別碼進行比較。It queries the System.Windows.Forms.Control.InvokeRequired property, which compares the control's creating thread ID to the calling thread ID. 如果執行緒識別碼相同,它會直接呼叫控制項。If the thread IDs are the same, it calls the control directly. 如果執行緒識別碼不同,它會使用主執行緒的委派呼叫 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 會將 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.ProgressChanged,並 BackgroundWorker.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