如何:對 Windows Forms 控制項進行安全線程呼叫

多執行緒可以改善 Windows Forms 應用程式的效能,但對 Windows Forms 控制項的存取原本就不是安全線程。 多執行緒可能會將您的程式碼公開至非常嚴重且複雜的 Bug。 操作控制項的兩個或多個執行緒可以強制控制項處於不一致的狀態,並導致競爭狀況、死結,以及凍結或停止回應。 如果您在應用程式中實作多執行緒,請務必以安全線程的方式呼叫跨執行緒控制項。 如需詳細資訊,請參閱 受控執行緒最佳做法

有兩種方式可從未建立該控制項的執行緒安全地呼叫 Windows Forms 控制項。 您可以使用 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)
{
    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 具有訊息的 ,跨執行緒作業無效, 來偵測這些不安全的執行緒呼叫。控制從建立執行緒以外的執行緒存取的 「」。 InvalidOperationException Visual Studio 偵錯期間,一律會發生不安全的跨執行緒呼叫,而且可能會在應用程式執行時間發生。 您應該修正此問題,但您可以將 屬性設定 Control.CheckForIllegalCrossThreadCallsfalse 來停用例外狀況。

保管庫跨執行緒呼叫

下列程式碼範例示範兩種方式,從未建立 Windows Forms 的執行緒安全地呼叫 Windows Forms 控制項:

  1. 方法 System.Windows.Forms.Control.Invoke ,它會從主執行緒呼叫委派來呼叫 控制項。
  2. System.ComponentModel.BackgroundWorker元件,提供事件驅動模型。

在這兩個範例中,背景執行緒會睡眠一秒,以模擬在該執行緒中完成的工作。

您可以從 C# 或 Visual Basic 命令列建置並執行這些範例作為 .NET Framework 應用程式。 如需詳細資訊,請參閱 使用 csc.exe 建置命令列或 從命令列建置 (Visual Basic)

從 .NET Core 3.0 開始,您也可以從具有 .NET Core Windows Forms < 資料夾 name.csproj > 專案檔的資料夾建置和執行範例作為 Windows .NET Core 應用程式。

範例:搭配委派使用 Invoke 方法

下列範例示範確保 Windows Forms 控制項安全呼叫執行緒的模式。 它會查詢 System.Windows.Forms.Control.InvokeRequired 屬性,它會比較控制項的建立執行緒識別碼與呼叫執行緒識別碼。 如果執行緒識別碼相同,它會直接呼叫 控制項。 如果執行緒識別碼不同,它會 Control.Invoke 使用主執行緒的委派呼叫 方法,以實際呼叫 控制項。

SafeCallDelegate會啟用設定 TextBox 控制項的 Text 屬性。 方法會 WriteTextSafe 查詢 InvokeRequired 。 如果 InvokeRequiredtrue 回 , WriteTextSafe 則會將 傳遞 SafeCallDelegateInvoke 方法,以對 控制項進行實際呼叫。 如果 InvokeRequiredfalse 回 , WriteTextSafe 則直接設定 TextBox.TextButton1_Click事件處理常式會建立新的執行緒並執行 WriteTextSafe 方法。

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 事件處理常式

實作多執行緒的簡單方式是搭配 System.ComponentModel.BackgroundWorker 使用事件驅動模型的元件。 背景執行緒會 BackgroundWorker.DoWork 執行事件,該事件不會與主執行緒互動。 主執行緒會執行 BackgroundWorker.ProgressChangedBackgroundWorker.RunWorkerCompleted 事件處理常式,其可以呼叫主執行緒的控制項。

若要使用 BackgroundWorker 進行安全線程呼叫,請在背景執行緒中建立方法來執行工作,並將它系結至 DoWork 事件。 在主執行緒中建立另一個方法,以報告背景工作的結果,並將它系結至 ProgressChangedRunWorkerCompleted 事件。 若要啟動背景執行緒,請呼叫 BackgroundWorker.RunWorkerAsync

此範例會 RunWorkerCompleted 使用 事件處理常式來設定 TextBox 控制項的 Text 屬性。 如需使用 ProgressChanged 事件的範例,請參閱 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

另請參閱