樣本
訪問 Windows 表單控制項本質上不是安全執行緒的。如果有兩個或多個線程操作某一控制項的狀態,則可能會迫使該控制項進入一種不一致的狀態。還可能出現其他與線程相關的 bug,包括爭用情況和死結。確保以安全執行緒方式訪問控制項非常重要。
.NET Framework 有助於在以非安全執行緒方式訪問控制項時檢測到這一問題。在調試器中運行應用程式時,如果建立某控制項的線程之外的其他線程試圖調用該控制項,則調試器會引發一個 InvalidOperationException,並提示訊息:“從不是建立控制項 control name 的線程訪問它。”
此異常在調試期間和運行時的某些情況下可靠地發生。強烈建議您在顯示此錯誤資訊時修複此問題。在調試以 .NET Framework 2.0 版之前的 .NET Framework 編寫的應用程式時,可能會出現此異常。
注意 |
可以通過將 CheckForIllegalCrossThreadCalls 屬性的值設定為 false 來禁用此異常。這會使控制項以與在 Visual Studio 2003 下相同的方式運行。 |
下面的程式碼範例示範如何從輔助線程以安全執行緒方式和非安全執行緒方式調用 Windows 表單控制項。它示範一種以非安全執行緒方式設定 TextBox 控制項的 Text 屬性的方法,還示範兩種以安全執行緒方式設定 Text 屬性的方法。
C#
using System;using System.ComponentModel;using System.Threading;using System.Windows.Forms;namespace CrossThreadDemo{public class Form1 : Form{// This delegate enables asynchronous calls for setting// the text property on a TextBox control.delegate void SetTextCallback(string text);// This thread is used to demonstrate both thread-safe and// unsafe ways to call a Windows Forms control.private Thread demoThread = null;// This BackgroundWorker is used to demonstrate the // preferred way of performing asynchronous operations.private BackgroundWorker backgroundWorker1;private TextBox textBox1;private Button setTextUnsafeBtn;private Button setTextSafeBtn;private Button setTextBackgroundWorkerBtn;private System.ComponentModel.IContainer components = null;public Form1(){InitializeComponent();}protected override void Dispose(bool disposing){if (disposing && (components != null)){components.Dispose();}base.Dispose(disposing);}// This event handler creates a thread that calls a // Windows Forms control in an unsafe way.private void setTextUnsafeBtn_Click(object sender,EventArgs e){this.demoThread =new Thread(new ThreadStart(this.ThreadProcUnsafe));this.demoThread.Start();}// This method is executed on the worker thread and makes// an unsafe call on the TextBox control.private void ThreadProcUnsafe(){this.textBox1.Text = "This text was set unsafely.";}// This event handler creates a thread that calls a // Windows Forms control in a thread-safe way.private void setTextSafeBtn_Click(object sender,EventArgs e){this.demoThread =new Thread(new ThreadStart(this.ThreadProcSafe));this.demoThread.Start();}// This method is executed on the worker thread and makes// a thread-safe call on the TextBox control.private void ThreadProcSafe(){this.SetText("This text was set safely.");}// This method demonstrates a pattern for making thread-safe// calls on a Windows Forms control. //// If the calling thread is different from the thread that// created the TextBox control, this method creates a// SetTextCallback and calls itself asynchronously using the// Invoke method.//// If the calling thread is the same as the thread that created// the TextBox control, the Text property is set directly. private void SetText(string text){// InvokeRequired required compares the thread ID of the// calling thread to the thread ID of the creating thread.// If these threads are different, it returns true.if (this.textBox1.InvokeRequired){SetTextCallback d = new SetTextCallback(SetText);this.Invoke(d, new object[] { text });}else{this.textBox1.Text = text;}}// This event handler starts the form's // BackgroundWorker by calling RunWorkerAsync.//// The Text property of the TextBox control is set// when the BackgroundWorker raises the RunWorkerCompleted// event.private void setTextBackgroundWorkerBtn_Click(object sender,EventArgs e){this.backgroundWorker1.RunWorkerAsync();}// This event handler sets the Text property of the TextBox// control. It is called on the thread that created the // TextBox control, so the call is thread-safe.//// BackgroundWorker is the preferred way to perform asynchronous// operations.private void backgroundWorker1_RunWorkerCompleted(object sender,RunWorkerCompletedEventArgs e){this.textBox1.Text ="This text was set safely by BackgroundWorker.";}#region Windows Form Designer generated codeprivate void InitializeComponent(){this.textBox1 = new System.Windows.Forms.TextBox();this.setTextUnsafeBtn = new System.Windows.Forms.Button();this.setTextSafeBtn = new System.Windows.Forms.Button();this.setTextBackgroundWorkerBtn = new System.Windows.Forms.Button();this.backgroundWorker1 = new System.ComponentModel.BackgroundWorker();this.SuspendLayout();// // textBox1// this.textBox1.Location = new System.Drawing.Point(12, 12);this.textBox1.Name = "textBox1";this.textBox1.Size = new System.Drawing.Size(240, 20);this.textBox1.TabIndex = 0;// // setTextUnsafeBtn// this.setTextUnsafeBtn.Location = new System.Drawing.Point(15, 55);this.setTextUnsafeBtn.Name = "setTextUnsafeBtn";this.setTextUnsafeBtn.TabIndex = 1;this.setTextUnsafeBtn.Text = "Unsafe Call";this.setTextUnsafeBtn.Click += new System.EventHandler(this.setTextUnsafeBtn_Click);// // setTextSafeBtn// this.setTextSafeBtn.Location = new System.Drawing.Point(96, 55);this.setTextSafeBtn.Name = "setTextSafeBtn";this.setTextSafeBtn.TabIndex = 2;this.setTextSafeBtn.Text = "Safe Call";this.setTextSafeBtn.Click += new System.EventHandler(this.setTextSafeBtn_Click);// // setTextBackgroundWorkerBtn// this.setTextBackgroundWorkerBtn.Location = new System.Drawing.Point(177, 55);this.setTextBackgroundWorkerBtn.Name = "setTextBackgroundWorkerBtn";this.setTextBackgroundWorkerBtn.TabIndex = 3;this.setTextBackgroundWorkerBtn.Text = "Safe BW Call";this.setTextBackgroundWorkerBtn.Click += new System.EventHandler(this.setTextBackgroundWorkerBtn_Click);// // backgroundWorker1// this.backgroundWorker1.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler(this.backgroundWorker1_RunWorkerCompleted);// // Form1// this.ClientSize = new System.Drawing.Size(268, 96);this.Controls.Add(this.setTextBackgroundWorkerBtn);this.Controls.Add(this.setTextSafeBtn);this.Controls.Add(this.setTextUnsafeBtn);this.Controls.Add(this.textBox1);this.Name = "Form1";this.Text = "Form1";this.ResumeLayout(false);this.PerformLayout();}#endregion[STAThread]static void Main(){Application.EnableVisualStyles();Application.Run(new Form1());}}}
對 Windows 表單控制項的非安全執行緒調用
對 Windows 表單控制項的非安全執行緒調用方式是從輔助線程直接調用。調用應用程式時,調試器會引發一個 InvalidOperationException,警告對控制項的調用不是安全執行緒的。
C#
// This event handler creates a thread that calls a // Windows Forms control in an unsafe way.private void setTextUnsafeBtn_Click(object sender,EventArgs e){this.demoThread =new Thread(new ThreadStart(this.ThreadProcUnsafe));this.demoThread.Start();}// This method is executed on the worker thread and makes// an unsafe call on the TextBox control.private void ThreadProcUnsafe(){this.textBox1.Text = "This text was set unsafely.";}
對 Windows 表單控制項的安全執行緒調用對 Windows 表單控制項進行安全執行緒調用
查詢控制項的 InvokeRequired 屬性。
如果 InvokeRequired 返回 true,則使用實際調用控制項的委託來調用 Invoke。
如果 InvokeRequired 返回 false,則直接調用控制項。
在下面的程式碼範例中,此邏輯是在一個稱為 SetText 的工具 + 生產力方法中實現的。名為 SetTextDelegate 的委託類型封裝 SetText 方法。TextBox 控制項的 InvokeRequired 返回 true 時,SetText 方法建立 SetTextDelegate 的一個執行個體,並調用表單的 Invoke 方法。這使得 SetText 方法被建立 TextBox 控制項的線程調用,而且在此線程上下文中將直接設定 Text 屬性。
C#
// This event handler creates a thread that calls a // Windows Forms control in a thread-safe way.private void setTextSafeBtn_Click(object sender,EventArgs e){this.demoThread =new Thread(new ThreadStart(this.ThreadProcSafe));this.demoThread.Start();}// This method is executed on the worker thread and makes// a thread-safe call on the TextBox control.private void ThreadProcSafe(){this.SetText("This text was set safely.");}
C#// This method demonstrates a pattern for making thread-safe
// calls on a Windows Forms control.
//
// If the calling thread is different from the thread that
// created the TextBox control, this method creates a
// SetTextCallback and calls itself asynchronously using the
// Invoke method.
//
// If the calling thread is the same as the thread that created
// the TextBox control, the Text property is set directly.
private void SetText(string text)
{
// InvokeRequired required compares the thread ID of the
// calling thread to the thread ID of the creating thread.
// If these threads are different, it returns true.
if (this.textBox1.InvokeRequired)
{
SetTextCallback d = new SetTextCallback(SetText);
this.Invoke(d, new object[] { text });
}
else
{
this.textBox1.Text = text;
}
}使用 BackgroundWorker 進行的安全執行緒調用
在應用程式中實現多線程的首選方式是使用 BackgroundWorker 組件。BackgroundWorker 組件使用事件驅動模型實現多線程。輔助線程運行 DoWork 事件處理常式,建立控制項的線程運行 ProgressChanged 和 RunWorkerCompleted 事件處理常式。注意不要從 DoWork 事件處理常式調用您的任何控制項。
下面的程式碼範例不非同步執行任何工作,因此沒有 DoWork 事件處理常式的實現。TextBox 控制項的 Text 屬性在 RunWorkerCompleted 事件處理常式中直接設定。
C#
// This event handler starts the form's // BackgroundWorker by calling RunWorkerAsync.//// The Text property of the TextBox control is set// when the BackgroundWorker raises the RunWorkerCompleted// event.private void setTextBackgroundWorkerBtn_Click(object sender,EventArgs e){this.backgroundWorker1.RunWorkerAsync();}// This event handler sets the Text property of the TextBox// control. It is called on the thread that created the // TextBox control, so the call is thread-safe.//// BackgroundWorker is the preferred way to perform asynchronous// operations.private void backgroundWorker1_RunWorkerCompleted(object sender,RunWorkerCompletedEventArgs e){this.textBox1.Text ="This text was set safely by BackgroundWorker.";}
Windows 表單上的 ActiveX 控制項
如果在表單上使用 ActiveX 控制項,則在調試器下運行時可能會收到線程間 InvalidOperationException。發生這種情況時,ActiveX 控制項不支援多執行緒。有關使用 Windows 表單的 ActiveX 控制項的更多資訊,請參見 Windows 表單和非託管應用程式。
如果您使用的是 Visual Studio,則可以通過禁用 Visual Studio 宿主進程來防止此異常發生。
可靠編程
警告 |
使用任何一種多線程時,代碼都容易產生非常嚴重而複雜的 bug。有關更多資訊,請在實現使用多線程的任何解決方案之前參見託管線程處理的最佳做法。 |
請參見任務
如何:在後台運行操作
如何:實現使用後台操作的表單
參考
BackgroundWorker
其他資源
使用 .NET Framework 開發自訂 Windows 表單控制項
Windows 表單和非託管應用程式