How to: Make Thread-Safe Calls to Windows Forms Controls
Posted on 2007-06-27 11:02 sunrack 阅读(597) 评论(0) 编辑 收藏 举报
If you use multithreading to improve the performance your Windows Forms applications, you must be careful to make calls to your controls in a thread-safe way.
Example
Access to Windows Forms controls is not inherently thread safe. If you have two or more threads manipulating the state of a control, it is possible to force the control into an inconsistent state. Other thread-related bugs are possible as well, including race conditions and deadlocks. It is important to ensure that access to your controls is done in a thread-safe way.
The .NET Framework helps you detect when you are accessing your controls in a manner that is not thread safe. When you are running your application in the debugger, and a thread other than the one which created a control attempts to call that control, the debugger raises an InvalidOperationException with the message, "Control control name accessed from a thread other than the thread it was created on."
This exception occurs reliably during debugging and, under some circumstances, at run time. You are strongly advised to fix this problem when you see it. You might see this exception when you debug applications that you wrote with the .NET Framework prior to .NET Framework version 2.0.
Note |
---|
You can disable this exception by setting the value of the CheckForIllegalCrossThreadCalls property to false. This causes your control to run the same way as it would run under Visual Studio 2003. |
The following code example shows how to call Windows Forms controls in a thread-safe manner and not in a thread-safe manner from a worker thread. It shows a way of setting the Text property of a TextBox control in a manner that is not thread safe, and it shows two thread-safe ways of setting the Text property.
Imports System Imports System.ComponentModel Imports System.Threading Imports System.Windows.Forms Public Class Form1 Inherits Form ' This delegate enables asynchronous calls for setting ' the text property on a TextBox control. Delegate Sub SetTextCallback([text] As String) ' This thread is used to demonstrate both thread-safe and ' unsafe ways to call a Windows Forms control. Private demoThread As Thread = Nothing ' This BackgroundWorker is used to demonstrate the ' preferred way of performing asynchronous operations. Private WithEvents backgroundWorker1 As BackgroundWorker Private textBox1 As TextBox Private WithEvents setTextUnsafeBtn As Button Private WithEvents setTextSafeBtn As Button Private WithEvents setTextBackgroundWorkerBtn As Button Private components As System.ComponentModel.IContainer = Nothing Public Sub New() InitializeComponent() End Sub Protected Overrides Sub Dispose(disposing As Boolean) If disposing AndAlso (components IsNot Nothing) Then components.Dispose() End If MyBase.Dispose(disposing) End Sub ' This event handler creates a thread that calls a ' Windows Forms control in an unsafe way. Private Sub setTextUnsafeBtn_Click( _ ByVal sender As Object, _ ByVal e As EventArgs) Handles setTextUnsafeBtn.Click Me.demoThread = New Thread( _ New ThreadStart(AddressOf Me.ThreadProcUnsafe)) Me.demoThread.Start() End Sub ' This method is executed on the worker thread and makes ' an unsafe call on the TextBox control. Private Sub ThreadProcUnsafe() Me.textBox1.Text = "This text was set unsafely." End Sub ' 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 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 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 #Region "Windows Form Designer generated code" Private Sub InitializeComponent() Me.textBox1 = New System.Windows.Forms.TextBox() Me.setTextUnsafeBtn = New System.Windows.Forms.Button() Me.setTextSafeBtn = New System.Windows.Forms.Button() Me.setTextBackgroundWorkerBtn = New System.Windows.Forms.Button() Me.backgroundWorker1 = New System.ComponentModel.BackgroundWorker() Me.SuspendLayout() ' ' textBox1 ' Me.textBox1.Location = New System.Drawing.Point(12, 12) Me.textBox1.Name = "textBox1" Me.textBox1.Size = New System.Drawing.Size(240, 20) Me.textBox1.TabIndex = 0 ' ' setTextUnsafeBtn ' Me.setTextUnsafeBtn.Location = New System.Drawing.Point(15, 55) Me.setTextUnsafeBtn.Name = "setTextUnsafeBtn" Me.setTextUnsafeBtn.TabIndex = 1 Me.setTextUnsafeBtn.Text = "Unsafe Call" ' ' setTextSafeBtn ' Me.setTextSafeBtn.Location = New System.Drawing.Point(96, 55) Me.setTextSafeBtn.Name = "setTextSafeBtn" Me.setTextSafeBtn.TabIndex = 2 Me.setTextSafeBtn.Text = "Safe Call" ' ' setTextBackgroundWorkerBtn ' Me.setTextBackgroundWorkerBtn.Location = New System.Drawing.Point(177, 55) Me.setTextBackgroundWorkerBtn.Name = "setTextBackgroundWorkerBtn" Me.setTextBackgroundWorkerBtn.TabIndex = 3 Me.setTextBackgroundWorkerBtn.Text = "Safe BW Call" ' ' backgroundWorker1 ' ' ' Form1 ' Me.ClientSize = New System.Drawing.Size(268, 96) Me.Controls.Add(setTextBackgroundWorkerBtn) Me.Controls.Add(setTextSafeBtn) Me.Controls.Add(setTextUnsafeBtn) Me.Controls.Add(textBox1) Me.Name = "Form1" Me.Text = "Form1" Me.ResumeLayout(False) Me.PerformLayout() End Sub 'InitializeComponent #End Region <STAThread()> _ Shared Sub Main() Application.EnableVisualStyles() Application.Run(New Form1()) End Sub End Class
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()); } } }
Calls to a Windows Forms Control That Are Not Thread Safe
The way to call a Windows Forms control that is not thread safe is to call directly from a worker thread. When you are debugging your application, the debugger raises an InvalidOperationException to warn you about calls to your controls that are not thread safe.
' This event handler creates a thread that calls a ' Windows Forms control in an unsafe way. Private Sub setTextUnsafeBtn_Click( _ ByVal sender As Object, _ ByVal e As EventArgs) Handles setTextUnsafeBtn.Click Me.demoThread = New Thread( _ New ThreadStart(AddressOf Me.ThreadProcUnsafe)) Me.demoThread.Start() End Sub ' This method is executed on the worker thread and makes ' an unsafe call on the TextBox control. Private Sub ThreadProcUnsafe() Me.textBox1.Text = "This text was set unsafely." End Sub
// 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."; }
Thread-Safe Calls to a Windows Forms Control
To make a thread-safe call a Windows Forms control
-
Query the control's InvokeRequired property.
-
If InvokeRequired returns true, call Invoke with a delegate that makes the actual call to the control.
-
If InvokeRequired returns false, call the control directly.
In the following code example, this logic is implemented in a utility method called SetText. A delegate type named SetTextDelegate encapsulates the SetText method. When the TextBox control's InvokeRequired returns true, the SetText method creates an instance of SetTextDelegate and calls the form's Invoke method. This causes the SetText method to be called on the thread that created the TextBox control, and in this thread context the Text property is set directly.
' 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 = 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 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 // 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; } }
Thread-Safe Calls with BackgroundWorker
The preferred way to implement multithreading in your application is to use the BackgroundWorker component. The BackgroundWorker component uses an event-driven model for multithreading. The worker thread runs your DoWork event handler, and the thread that creates your controls runs your ProgressChanged and RunWorkerCompleted event handlers. Be careful not to call any of your controls from your DoWork event handler.
In the following code example, there is no work to perform asynchronously, so there is no DoWork event handler implementation. The TextBox control's Text property is set directly in the RunWorkerCompleted event handler.
' 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."; }
ActiveX Controls on Windows Forms
If you are using ActiveX controls on a form, you may receive the cross-thread InvalidOperationException when running under the debugger. When this occurs, the ActiveX control does not support multi-threading. For more information about using ActiveX controls with Windows Forms, see Windows Forms and Unmanaged Applications.
If you are using Visual Studio, you can prevent this exception from occurring by disabling the Visual Studio hosting process.
For more information, see How to: Disable the Hosting Process and How to: Disable the Hosting Process.
Robust Programming
Caution |
---|
When you use multithreading of any sort, your code can be exposed to very serious and complex bugs. For more information, see Managed Threading Best Practices before implementing any solution that uses multithreading. |