如何:对 Windows 窗体控件进行线程安全调用
.NET Framework (current version)
其他版本

@H_301_21@
?
发布时间: 2016年5月
如果使用多线程处理来提高 Windows 窗体应用程序的性能,则你必须确保以线程安全的方式调用控件。
访问 Windows 窗体控件不是本身就线程安全的。?如果有两个或两个以上线程操作控件的状态,则可能迫使该控件处于不一致状态。?可能出现其他与线程相关的 bug,例如争用条件和死锁。?请务必确保以线程安全的方式访问控件。
从未使用?Invoke?方法创建控件的线程调用控件是不安全的。?下面是一个非线程安全的调用示例。
C#
C++
VB
// 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.
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 窗体控件进行线程安全的调用
如需对 Windows 窗体控件进行线程安全的调用
-
查询控件的?InvokeRequired?属性。
-
若?InvokeRequired?返回?true,则用实际调用控件的委托来调用?Invoke。
-
若?InvokeRequired?返回?false,则请直接调用控件。
在以下代码示例中,在?ThreadProcSafe?方法中实现了线程安全的调用,该方法由后台线程执行。?若?TextBox?控件的?InvokeRequired?返回true,则?ThreadProcSafe?方法创建一个?SetTextCallback?实例并将其传递到窗体的?Invoke?方法。?这导致在创建了?SetText?控件的线程上调用TextBox?方法,并且在该线程上下文中直接设置?Text?属性。
C#
// Windows Forms control in a thread-safe way.
void setTextSafeBtn_Click(
this.ThreadProcSafe));
// a thread-safe call on the TextBox control.
void ThreadProcSafe()
{
this.SetText("This text was set safely.");
}
VB
' 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.
Sub ThreadProcSafe()
Me.SetText("This text was set safely.")
Sub
C++
// This event handler creates a thread that calls a
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
void ThreadProcSafe()
{
this->SetText("This text was set safely.");
}
// the text property on a TextBox control.
delegate void SetTextCallback(string text);
' the text property on a TextBox control.
Delegate Sub SetTextCallback([text] String)
// 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.
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;
}
}
' calls on a Windows Forms control.
'
' If the calling thread is different from the thread that
' created the TextBox control,sans-serif!important; color:green'>' 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.
Sub SetText(ByVal [text] 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 New SetTextCallback(AddressOf SetText)
Me.Invoke(d,sans-serif!important; color:blue'>New Object() {[text]})
Else
Me.textBox1.Text = [text]
If
Sub
// calls on a Windows Forms control.
//
// If the calling thread is different from the thread that
// SetTextDelegate and calls itself asynchronously using the
// Invoke method.
// If the calling thread is the same as the thread that created
void SetText(String^ text)
{
// Invokerequired required compares the thread ID of the
// calling thread to the thread ID of the creating thread.
this->textBox1->Invokerequired)
{
SetTextDelegate^ d =
gcnew SetTextDelegate(this->Invoke(d,sans-serif!important; color:blue'>gcnew array<Object^> { text });
}
else
{
this->textBox1->Text = text;
}
}
通过使用 BackgroundWorker 进行线程安全的调用
在应用程序中实现多线程的首选方式是使用?BackgroundWorker?组件。?BackgroundWorker?组件为多线程处理使用事件驱动模型。?后台线程运行你的?DoWork?事件处理程序,创建了你的控件的线程运行?ProgressChanged?和?RunWorkerCompleted?事件处理程序。?你可以从RunWorkerCompleted?事件处理器中调用控件。
如需通过使用 BackgroundWorker 进行线程安全的调用
-
创建一种方法来进行你想在后台线程中进行的工作。?不要调用由此方法中的主线程所创建的控件。
-
创建一种方法来报告后台工作结束后的后台工作结果。?在此方法中可以调用主线程创建的控件。
-
将步骤 1 中创建的方法绑定到?DoWork?实例中的?BackgroundWorker?事件,并将步骤 2 中创建的方法绑定到同一实例的RunWorkerCompleted?事件。
-
若要启动后台线程,请调用?RunWorkerAsync?实例的?BackgroundWorker?方法。
在以下代码示例中,DoWork?事件处理程序使用?Sleep?来模拟需要花费一些时间的工作。?它不会调用该窗体的?TextBox?控件。?TextBox?控件的Text?属性直接在?RunWorkerCompleted?事件处理程序中设置。
// preferred way of performing asynchronous operations.
private BackgroundWorker backgroundWorker1;
' preferred way of performing asynchronous operations.
WithEvents backgroundWorker1 As BackgroundWorker
private:
BackgroundWorker^ backgroundWorker1;
// BackgroundWorker by calling RunWorkerAsync.
// The Text property of the TextBox control is set
// when the BackgroundWorker raises the RunWorkerCompleted
// event.
void setTextBackgroundWorkerBtn_Click(
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.
void backgroundWorker1_RunWorkerCompleted(
this.textBox1.Text =
"This text was set safely by BackgroundWorker.";
}
' BackgroundWorker by calling RunWorkerAsync.
' The Text property of the TextBox control is set
' when the BackgroundWorker raises the RunWorkerCompleted
' event.
Sub setTextBackgroundWorkerBtn_Click( _
Handles setTextBackgroundWorkerBtn.Click
Me.backgroundWorker1.RunWorkerAsync()
' This event handler sets the Text property of the TextBox
' control. It is called on the thread that created the
' TextBox control,sans-serif!important; color:green'>' BackgroundWorker is the preferred way to perform asynchronous
' operations.
Sub backgroundWorker1_RunWorkerCompleted( _
As RunWorkerCompletedEventArgs) _
Handles backgroundWorker1.RunWorkerCompleted
Me.textBox1.Text = _
"This text was set safely by BackgroundWorker."
Sub
// BackgroundWorker by calling RunWorkerAsync.
// The Text property of the TextBox control is set
// when the BackgroundWorker raises the RunWorkerCompleted
void setTextBackgroundWorkerBtn_Click(Object^ sender,sans-serif!important; color:blue'>this->backgroundWorker1->RunWorkerAsync();
}
// This event handler sets the Text property of the TextBox
// control. It is called on the thread that created the
// BackgroundWorker is the preferred way to perform asynchronous
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.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.
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 = 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.
void setTextUnsafeBtn_Click(
this.demoThread =
this.ThreadProcUnsafe));
this.demoThread.Start();
}
// This method is executed on the worker thread and makes
// an unsafe call on the TextBox control.
void ThreadProcUnsafe()
{
"This text was set unsafely.";
}
// Windows Forms control in a thread-safe way.
void setTextSafeBtn_Click(
this.ThreadProcSafe));
// a thread-safe call on the TextBox control.
void ThreadProcSafe()
{
"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
// SetTextCallback and calls itself asynchronously using the
// Invoke method.
// If the calling thread is the same as the thread that created
string text)
{
// Invokerequired required compares the thread ID of the
// calling thread to the thread ID of the creating thread.
this.textBox1.Invokerequired)
{
SetTextCallback d = new SetTextCallback(SetText);
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.
void setTextBackgroundWorkerBtn_Click(
this.backgroundWorker1.RunWorkerAsync();
}
// This event handler sets the Text property of the TextBox
// control. It is called on the thread that created the
// BackgroundWorker is the preferred way to perform asynchronous
// operations.
void backgroundWorker1_RunWorkerCompleted(
this.textBox1.Text =
"This text was set safely by BackgroundWorker.";
}
#region Windows Form Designer generated code
void InitializeComponent()
{
this.textBox1 = new System.Windows.Forms.TextBox();
this.setTextUnsafeBtn = new System.Windows.Forms.Button();
this.setTextSafeBtn = this.setTextBackgroundWorkerBtn = 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,sans-serif!important; color:blue'>this.setTextSafeBtn.Name = "setTextSafeBtn";
this.setTextSafeBtn.TabIndex = 2;
this.setTextSafeBtn.Text = "Safe Call";
this.setTextSafeBtn.Click += this.setTextSafeBtn_Click);
// setTextBackgroundWorkerBtn
this.setTextBackgroundWorkerBtn.Location = new System.Drawing.Point(177,sans-serif!important; color:blue'>this.setTextBackgroundWorkerBtn.Name = "setTextBackgroundWorkerBtn";
this.setTextBackgroundWorkerBtn.TabIndex = 3;
this.setTextBackgroundWorkerBtn.Text = "Safe BW Call";
this.setTextBackgroundWorkerBtn.Click += 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.setTextSafeBtn);
this.setTextUnsafeBtn);
this.textBox1);
this.Name = "Form1";
this.Text = this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.Run(new Form1());
}
}
}
运行应用程序并单击“不安全调用”按钮时,你可以立即在文本框中看到“由主线程写入”。?两秒钟之后,尝试进行不安全调用时,Visual Studio 调试器指示发生了异常。?调试器在后台线程中试图直接写入文本框的那一行停止。?你将必须重新启动该应用程序来测试其他两个按钮。?单击“安全调用”按钮时,文本框中显示“由主线程写入”。?两秒钟之后,文本框中被设置为“由后台线程写入 (Invoke)”,表示调用了?Invoke?方法。?单击“安全 BW 调用”按钮时,文本框中显示“由主线程写入”。?两秒钟之后,文本框被设置为“在后台线程完成后由主线程写入”,表示调用了RunWorkerCompleted?的?BackgroundWorker?事件的处理程序。
可靠编程

小心
使用任何种类的多线程时,都有可能会遇到非常严重且复杂的 bug。?有关详细信息,请在实现使用多线程处理的任何解决方案之前参阅Managed Threading Best Practices。
(编辑:北几岛)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!