如何:對 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.CheckForIllegalCrossThreadCalls 為 false
來停用例外狀況。
保管庫跨執行緒呼叫
下列程式碼範例示範兩種方式,從未建立 Windows Forms 的執行緒安全地呼叫 Windows Forms 控制項:
- 方法 System.Windows.Forms.Control.Invoke ,它會從主執行緒呼叫委派來呼叫 控制項。
- 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 。 如果 InvokeRequired 傳 true
回 , WriteTextSafe
則會將 傳遞 SafeCallDelegate
至 Invoke 方法,以對 控制項進行實際呼叫。 如果 InvokeRequired 傳 false
回 , WriteTextSafe
則直接設定 TextBox.Text 。 Button1_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.ProgressChanged 和 BackgroundWorker.RunWorkerCompleted 事件處理常式,其可以呼叫主執行緒的控制項。
若要使用 BackgroundWorker 進行安全線程呼叫,請在背景執行緒中建立方法來執行工作,並將它系結至 DoWork 事件。 在主執行緒中建立另一個方法,以報告背景工作的結果,並將它系結至 ProgressChanged 或 RunWorkerCompleted 事件。 若要啟動背景執行緒,請呼叫 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
另請參閱
意見反應
https://aka.ms/ContentUserFeedback。
即將登場:在 2024 年,我們將逐步淘汰 GitHub 問題作為內容的意見反應機制,並將它取代為新的意見反應系統。 如需詳細資訊,請參閱:提交並檢視相關的意見反應