Share via


如何對控制項進行安全線程呼叫 (Windows Forms .NET)

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

重要

.NET 7 和 .NET 6 的桌面指南檔正在建置中。

有兩種方式可從未建立該控制項的執行緒安全地呼叫 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)
{
    var thread2 = new System.Threading.Thread(WriteTextUnsafe);
    thread2.Start();
}

private void WriteTextUnsafe() =>
    textBox1.Text = "This text was set unsafely.";
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    Dim thread2 As New System.Threading.Thread(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元件,提供事件驅動模型。

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

範例:使用 Invoke 方法

下列範例示範確保 Windows Forms 控制項安全呼叫執行緒的模式。 它會查詢 System.Windows.Forms.Control.InvokeRequired 屬性,它會比較控制項的建立執行緒識別碼與呼叫執行緒識別碼。 如果它們不同,您應該呼叫 Control.Invoke 方法。

WriteTextSafe 啟用將 TextBox 控制項的 Text 屬性設定為新的值。 方法會查詢 InvokeRequired 。 如果 InvokeRequiredtrue 回 , WriteTextSafe 則以遞迴方式呼叫本身,將 方法當做委派傳遞至 Invoke 方法。 如果 InvokeRequiredfalse 回 , WriteTextSafe 則直接設定 TextBox.TextButton1_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;
}
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

    Dim threadParameters As New System.Threading.ThreadStart(Sub()
                                                                 WriteTextSafe("This text was set safely.")
                                                             End Sub)

    Dim thread2 As New System.Threading.Thread(threadParameters)
    thread2.Start()

End Sub

Private Sub WriteTextSafe(text As String)

    If (TextBox1.InvokeRequired) Then

        TextBox1.Invoke(Sub()
                            WriteTextSafe($"{text} (THREAD2)")
                        End Sub)

    Else
        TextBox1.Text = text
    End If

End Sub

範例:使用 BackgroundWorker

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

若要使用 BackgroundWorker 進行安全線程呼叫,請處理 DoWork 事件。 背景工作者用來報告狀態的事件有兩個: ProgressChangedRunWorkerCompleted 。 事件 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;
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

    If (Not BackgroundWorker1.IsBusy) Then
        BackgroundWorker1.RunWorkerAsync()
    End If

End Sub

Private Sub BackgroundWorker1_DoWork(sender As Object, e As ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork

    Dim counter = 0
    Dim max = 10

    While counter <= max

        BackgroundWorker1.ReportProgress(0, counter.ToString())
        System.Threading.Thread.Sleep(1000)

        counter += 1

    End While

End Sub

Private Sub BackgroundWorker1_ProgressChanged(sender As Object, e As ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
    TextBox1.Text = e.UserState
End Sub