如何:对 Windows 窗体控件进行线程安全调用
发布时间: 2016年5月
如果使用多线程处理来提高 Windows 窗体应用程序的性能,则你必须确保以线程安全的方式调用控件。
访问 Windows 窗体控件不是本身就线程安全的。 如果有两个或两个以上线程操作控件的状态,则可能迫使该控件处于不一致状态。 可能出现其他与线程相关的 bug,例如争用条件和死锁。 请务必确保以线程安全的方式访问控件。
从未使用 Invoke 方法创建控件的线程调用控件是不安全的。 下面是一个非线程安全的调用示例。
// 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."; }
.NET Framework 可帮助检测你是否以线程安全的方式访问控件。 在调试器中运行应用程序并且未创建控件的线程试图调用控件时,调试器会引发InvalidOperationException 消息,“从并未创建该控件的线程访问该控件 控件名称”。
在调试过程中、在某些情况下以及在运行时均极有可能发生此异常。 当你调试在 .NET Framework 之前用 .NET Framework 2.0 编写的应用程序时可能会看到此异常。 强烈建议你在遇到此问题时修复它,但你可通过将 CheckForIllegalCrossThreadCalls 属性设置为 false 来禁用它。 这使控件可像在 Visual Studio .NET 2003 和 .NET Framework 1.1 下那样运行。
注意 |
---|
如果你使用的是窗体上的 ActiveX 控件,则在调试器下运行时可能会收到跨线程 InvalidOperationException。 发生此情况时,ActiveX 控件不支持多线程处理。 有关使用 Windows 窗体的 ActiveX 控件的详细信息,请参阅 Windows 窗体和非托管应用程序。 如果你使用的是 Visual Studio,则可通过禁用 Visual Studio 的托管进程来避免此异常,请参阅如何:禁用承载进程。 |
如需对 Windows 窗体控件进行线程安全的调用
-
查询控件的 InvokeRequired 属性。
-
若 InvokeRequired 返回 true,则用实际调用控件的委托来调用 Invoke。
-
若 InvokeRequired 返回 false,则请直接调用控件。
在以下代码示例中,在 ThreadProcSafe 方法中实现了线程安全的调用,该方法由后台线程执行。 若 TextBox 控件的 InvokeRequired 返回true,则 ThreadProcSafe 方法创建一个 SetTextCallback 实例并将其传递到窗体的 Invoke 方法。 这导致在创建了 SetText 控件的线程上调用TextBox 方法,并且在该线程上下文中直接设置 Text 属性。
// 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 event handler creates a thread that calls a ' Windows Forms control in a thread-safe way. Private Sub setTextSafeBtn_Click( _ ByVal sender As Object, _ ByVal e As EventArgs) Handles setTextSafeBtn.Click Me.demoThread = New Thread( _ New ThreadStart(AddressOf Me.ThreadProcSafe)) Me.demoThread.Start() End Sub ' This method is executed on the worker thread and makes ' a thread-safe call on the TextBox control. Private Sub ThreadProcSafe() Me.SetText("This text was set safely.") End Sub
// 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 = gcnew Thread(gcnew ThreadStart(this,&Form1::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 delegate enables asynchronous calls for setting // the text property on a TextBox control. delegate void SetTextCallback(string text);
' This delegate enables asynchronous calls for setting ' the text property on a TextBox control. Delegate Sub SetTextCallback([text] As String)
// This delegate enables asynchronous calls for setting // the text property on a TextBox control. delegate void SetTextDelegate(String^ text);
// 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 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 Sub SetText(ByVal [text] As String) ' 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 Me.textBox1.InvokeRequired Then Dim d As New SetTextCallback(AddressOf SetText) Me.Invoke(d, New Object() {[text]}) Else Me.textBox1.Text = [text] End If End Sub
// 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 // SetTextDelegate 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) { SetTextDelegate^ d = gcnew SetTextDelegate(this, &Form1::SetText); this->Invoke(d, gcnew array<Object^> { text }); } else { this->textBox1->Text = text; } }
在应用程序中实现多线程的首选方式是使用 BackgroundWorker 组件。 BackgroundWorker 组件为多线程处理使用事件驱动模型。 后台线程运行你的 DoWork 事件处理程序,创建了你的控件的线程运行 ProgressChanged 和 RunWorkerCompleted 事件处理程序。 你可以从ProgressChanged 和 RunWorkerCompleted 事件处理器中调用控件。
如需通过使用 BackgroundWorker 进行线程安全的调用
-
创建一种方法来进行你想在后台线程中进行的工作。 不要调用由此方法中的主线程所创建的控件。
-
创建一种方法来报告后台工作结束后的后台工作结果。 在此方法中可以调用主线程创建的控件。
-
将步骤 1 中创建的方法绑定到 DoWork 实例中的 BackgroundWorker 事件,并将步骤 2 中创建的方法绑定到同一实例的RunWorkerCompleted 事件。
-
若要启动后台线程,请调用 RunWorkerAsync 实例的 BackgroundWorker 方法。
在以下代码示例中,DoWork 事件处理程序使用 Sleep 来模拟需要花费一些时间的工作。 它不会调用该窗体的 TextBox 控件。 TextBox 控件的Text 属性直接在 RunWorkerCompleted 事件处理程序中设置。
// This BackgroundWorker is used to demonstrate the // preferred way of performing asynchronous operations. private BackgroundWorker backgroundWorker1;
' This BackgroundWorker is used to demonstrate the ' preferred way of performing asynchronous operations. Private WithEvents backgroundWorker1 As BackgroundWorker
// This BackgroundWorker is used to demonstrate the // preferred way of performing asynchronous operations. private: BackgroundWorker^ backgroundWorker1;
// 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."; }
' 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 Sub setTextBackgroundWorkerBtn_Click( _ ByVal sender As Object, _ ByVal e As EventArgs) Handles setTextBackgroundWorkerBtn.Click Me.backgroundWorker1.RunWorkerAsync() End Sub ' 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 Sub backgroundWorker1_RunWorkerCompleted( _ ByVal sender As Object, _ ByVal e As RunWorkerCompletedEventArgs) _ Handles backgroundWorker1.RunWorkerCompleted Me.textBox1.Text = _ "This text was set safely by BackgroundWorker." End Sub
// 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."; }
也可通过使用 ProgressChanged 事件来报告后台任务的进度。 如需包含该事件的示例,请参阅 BackgroundWorker。
示例
以下代码示例是一个完整的 Windows 窗体应用程序,由带有三个按钮和一个文本框的窗体组成。 第一个按钮演示了不安全的跨线程访问,第二个按钮使用 Invoke 演示了安全的访问,第三个按钮通过使用 BackgroundWorker 演示了安全的访问。
注意 |
---|
有关如何运行该示例的说明,请参阅如何:使用 Visual Studio 编译和运行完整的 Windows 窗体代码示例。 该示例需引用 System.Drawing 和 System.Windows.Forms 程序集。 |
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 code private 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()); } } }
可靠编程
小心 |
---|
使用任何种类的多线程时,都有可能会遇到非常严重且复杂的 bug。 有关详细信息,请在实现使用多线程处理的任何解决方案之前参阅Managed Threading Best Practices。 |
运行应用程序并单击“不安全调用”按钮时,你可以立即在文本框中看到“由主线程写入”。 两秒钟之后,尝试进行不安全调用时,Visual Studio 调试器指示发生了异常。 调试器在后台线程中试图直接写入文本框的那一行停止。 你将必须重新启动该应用程序来测试其他两个按钮。 单击“安全调用”按钮时,文本框中显示“由主线程写入”。 两秒钟之后,文本框中被设置为“由后台线程写入 (Invoke)”,表示调用了 Invoke 方法。 单击“安全 BW 调用”按钮时,文本框中显示“由主线程写入”。 两秒钟之后,文本框被设置为“在后台线程完成后由主线程写入”,表示调用了RunWorkerCompleted 的 BackgroundWorker 事件的处理程序。