如何:对 Windows 窗体控件进行线程安全调用

如何:对 Windows 窗体控件进行线程安全调用

.NET Framework (current version)
 

发布时间: 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 下那样运行。

System_CAPS_note注意

如果你使用的是窗体上的 ActiveX 控件,则在调试器下运行时可能会收到跨线程 InvalidOperationException 发生此情况时,ActiveX 控件不支持多线程处理。 有关使用 Windows 窗体的 ActiveX 控件的详细信息,请参阅 Windows 窗体和非托管应用程序 如果你使用的是 Visual Studio,则可通过禁用 Visual Studio 的托管进程来避免此异常,请参阅如何:禁用承载进程

如需对 Windows 窗体控件进行线程安全的调用

  1. 查询控件的 InvokeRequired 属性。

  2. 若 InvokeRequired 返回 true,则用实际调用控件的委托来调用 Invoke

  3. 若 InvokeRequired 返回 false,则请直接调用控件。

在以下代码示例中,在 ThreadProcSafe 方法中实现了线程安全的调用,该方法由后台线程执行。 若 TextBox 控件的 InvokeRequired 返回true,则 ThreadProcSafe 方法创建一个 SetTextCallback 实例并将其传递到窗体的 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.");
}
VB
  ' 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
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 =
            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.");
    }
C#
  // This delegate enables asynchronous calls for setting
// the text property on a TextBox control.
delegate void SetTextCallback(string text);
VB
  ' This delegate enables asynchronous calls for setting
' the text property on a TextBox control.
Delegate Sub SetTextCallback([text] As String)
C++
  // This delegate enables asynchronous calls for setting
// the text property on a TextBox control.
delegate void SetTextDelegate(String^ text);
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;
	}
}
VB
  ' 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
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
    // 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. 创建一种方法来进行你想在后台线程中进行的工作。 不要调用由此方法中的主线程所创建的控件。

  2. 创建一种方法来报告后台工作结束后的后台工作结果。 在此方法中可以调用主线程创建的控件。

  3. 将步骤 1 中创建的方法绑定到 DoWork 实例中的 BackgroundWorker 事件,并将步骤 2 中创建的方法绑定到同一实例的RunWorkerCompleted 事件。

  4. 若要启动后台线程,请调用 RunWorkerAsync 实例的 BackgroundWorker 方法。

在以下代码示例中,DoWork 事件处理程序使用 Sleep 来模拟需要花费一些时间的工作。 它不会调用该窗体的 TextBox 控件。 TextBox 控件的Text 属性直接在 RunWorkerCompleted 事件处理程序中设置。

C#
  // This BackgroundWorker is used to demonstrate the 
// preferred way of performing asynchronous operations.
private BackgroundWorker backgroundWorker1;
VB
  ' This BackgroundWorker is used to demonstrate the 
' preferred way of performing asynchronous operations.
Private WithEvents backgroundWorker1 As BackgroundWorker
C++
    // This BackgroundWorker is used to demonstrate the
    // preferred way of performing asynchronous operations.
private:
    BackgroundWorker^ backgroundWorker1;
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.";
}
VB
  ' 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
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.";
    }

也可通过使用 ProgressChanged 事件来报告后台任务的进度。 如需包含该事件的示例,请参阅 BackgroundWorker

示例

以下代码示例是一个完整的 Windows 窗体应用程序,由带有三个按钮和一个文本框的窗体组成。 第一个按钮演示了不安全的跨线程访问,第二个按钮使用 Invoke 演示了安全的访问,第三个按钮通过使用 BackgroundWorker 演示了安全的访问。

System_CAPS_note注意

有关如何运行该示例的说明,请参阅如何:使用 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());
		}

	}
}

运行应用程序并单击“不安全调用”按钮时,你可以立即在文本框中看到“由主线程写入”。 两秒钟之后,尝试进行不安全调用时,Visual Studio 调试器指示发生了异常。 调试器在后台线程中试图直接写入文本框的那一行停止。 你将必须重新启动该应用程序来测试其他两个按钮。 单击“安全调用”按钮时,文本框中显示“由主线程写入”。 两秒钟之后,文本框中被设置为“由后台线程写入 (Invoke)”,表示调用了 Invoke 方法。 单击“安全 BW 调用”按钮时,文本框中显示“由主线程写入”。 两秒钟之后,文本框被设置为“在后台线程完成后由主线程写入”,表示调用了RunWorkerCompleted 的 BackgroundWorker 事件的处理程序。

可靠编程

System_CAPS_caution小心

使用任何种类的多线程时,都有可能会遇到非常严重且复杂的 bug。 有关详细信息,请在实现使用多线程处理的任何解决方案之前参阅Managed Threading Best Practices

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Windows窗体应用程序中,如果多个线程需要同时访问同一个控件(如Label、TextBox等),就需要使用线程安全的方法来访问控件,以避免出现访问冲突和其他线程安全问题。 以下是一个示例,展示如何以线程安全的方式访问跨线程控件: ```csharp public partial class Form1 : Form { private delegate void UpdateTextDelegate(string text); public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { // 创建一个新线程 Thread thread = new Thread(new ThreadStart(ThreadMethod)); thread.Start(); } private void ThreadMethod() { // 线程执行的代码 // ... // 调用UpdateText方法更新跨线程控件 string text = "Hello from thread " + Thread.CurrentThread.ManagedThreadId; UpdateText(text); } private void UpdateText(string text) { // 如果当前线程不是UI线程,则通过Invoke方法调用UpdateTextDelegate委托 if (this.InvokeRequired) { UpdateTextDelegate updateText = new UpdateTextDelegate(UpdateText); this.Invoke(updateText, new object[] { text }); } else { // 否则直接更新控件 label1.Text = text; } } } ``` 在上面的示例中,我们创建了一个新线程,并在该线程中调用了ThreadMethod方法。在ThreadMethod方法中,我们调用了UpdateText方法来更新跨线程控件。在UpdateText方法中,我们首先检查当前线程是否是UI线程,如果不是,则通过Invoke方法调用UpdateTextDelegate委托来在UI线程中更新控件;如果是,则直接更新控件。 通过这种方式,我们可以实现多线程处理,并且以线程安全的方式访问跨线程控件
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值