操作說明︰對 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 (部分機器翻譯) 來偵測這些不安全的執行緒呼叫:跨執行緒作業無效。存取控制項 "" 時所使用的執行緒與用來建立控制項的執行緒不同。在 Visual Studio 偵錯期間,不安全的跨執行緒呼叫一律會發生 InvalidOperationException (部分機器翻譯),而且在應用程式執行階段也可能會發生。 您應該修正此問題,但您也可以藉由將 Control.CheckForIllegalCrossThreadCalls (部分機器翻譯) 屬性設定為 false
來停用例外狀況。
安全的跨執行緒呼叫
下列程式碼範例會示範兩種可從未建立 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 <資料夾名稱>.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