How to make thread-safe calls to controls (Windows Forms .NET)
Multithreading can improve the performance of Windows Forms apps, but access to Windows Forms controls isn't inherently thread-safe. Multithreading can expose your code to serious and complex bugs. Two or more threads manipulating a control can force the control into an inconsistent state and lead to race conditions, deadlocks, and freezes or hangs. If you implement multithreading in your app, be sure to call cross-thread controls in a thread-safe way. For more information, see Managed threading best practices.
There are two ways to safely call a Windows Forms control from a thread that didn't create that control. Use the System.Windows.Forms.Control.Invoke method to call a delegate created in the main thread, which in turn calls the control. Or, implement a System.ComponentModel.BackgroundWorker, which uses an event-driven model to separate work done in the background thread from reporting on the results.
Unsafe cross-thread calls
It's unsafe to call a control directly from a thread that didn't create it. The following code snippet illustrates an unsafe call to the System.Windows.Forms.TextBox control. The Button1_Click
event handler creates a new WriteTextUnsafe
thread, which sets the main thread's TextBox.Text property directly.
private void button1_Click(object sender, EventArgs e)
{
var thread2 = new System.Threading.Thread(WriteTextUnsafe);
thread2.Start();
}
private void WriteTextUnsafe() =>
textBox1.Text = "This text was set unsafely.";
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim thread2 As New System.Threading.Thread(AddressOf WriteTextUnsafe)
thread2.Start()
End Sub
Private Sub WriteTextUnsafe()
TextBox1.Text = "This text was set unsafely."
End Sub
The Visual Studio debugger detects these unsafe thread calls by raising an InvalidOperationException with the message, Cross-thread operation not valid. Control accessed from a thread other than the thread it was created on. The InvalidOperationException always occurs for unsafe cross-thread calls during Visual Studio debugging, and may occur at app runtime. You should fix the issue, but you can disable the exception by setting the Control.CheckForIllegalCrossThreadCalls property to false
.
Safe cross-thread calls
The following code examples demonstrate two ways to safely call a Windows Forms control from a thread that didn't create it:
- The System.Windows.Forms.Control.Invoke method, which calls a delegate from the main thread to call the control.
- A System.ComponentModel.BackgroundWorker component, which offers an event-driven model.
In both examples, the background thread sleeps for one second to simulate work being done in that thread.
Example: Use the Invoke method
The following example demonstrates a pattern for ensuring thread-safe calls to a Windows Forms control. It queries the System.Windows.Forms.Control.InvokeRequired property, which compares the control's creating thread ID to the calling thread ID. If they're different, you should call the Control.Invoke method.
The WriteTextSafe
enables setting the TextBox control's Text property to a new value. The method queries InvokeRequired. If InvokeRequired returns true
, WriteTextSafe
recursively calls itself, passing the method as a delegate to the Invoke method. If InvokeRequired returns false
, WriteTextSafe
sets the TextBox.Text directly. The Button1_Click
event handler creates the new thread and runs the WriteTextSafe
method.
private void button1_Click(object sender, EventArgs e)
{
var threadParameters = new System.Threading.ThreadStart(delegate { WriteTextSafe("This text was set safely."); });
var thread2 = new System.Threading.Thread(threadParameters);
thread2.Start();
}
public void WriteTextSafe(string text)
{
if (textBox1.InvokeRequired)
{
// Call this same method but append THREAD2 to the text
Action safeWrite = delegate { WriteTextSafe($"{text} (THREAD2)"); };
textBox1.Invoke(safeWrite);
}
else
textBox1.Text = text;
}
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim threadParameters As New System.Threading.ThreadStart(Sub()
WriteTextSafe("This text was set safely.")
End Sub)
Dim thread2 As New System.Threading.Thread(threadParameters)
thread2.Start()
End Sub
Private Sub WriteTextSafe(text As String)
If (TextBox1.InvokeRequired) Then
TextBox1.Invoke(Sub()
WriteTextSafe($"{text} (THREAD2)")
End Sub)
Else
TextBox1.Text = text
End If
End Sub
Example: Use a BackgroundWorker
An easy way to implement multithreading is with the System.ComponentModel.BackgroundWorker component, which uses an event-driven model. The background thread raises the BackgroundWorker.DoWork event, which doesn't interact with the main thread. The main thread runs the BackgroundWorker.ProgressChanged and BackgroundWorker.RunWorkerCompleted event handlers, which can call the main thread's controls.
To make a thread-safe call by using BackgroundWorker, handle the DoWork event. There are two events the background worker uses to report status: ProgressChanged and RunWorkerCompleted. The ProgressChanged
event is used to communicate status updates to the main thread, and the RunWorkerCompleted
event is used to signal that the background worker has completed its work. To start the background thread, call BackgroundWorker.RunWorkerAsync.
The example counts from 0 to 10 in the DoWork
event, pausing for one second between counts. It uses the ProgressChanged event handler to report the number back to the main thread and set the TextBox control's Text property. For the ProgressChanged event to work, the BackgroundWorker.WorkerReportsProgress property must be set to true
.
private void button1_Click(object sender, EventArgs e)
{
if (!backgroundWorker1.IsBusy)
backgroundWorker1.RunWorkerAsync();
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
int counter = 0;
int max = 10;
while (counter <= max)
{
backgroundWorker1.ReportProgress(0, counter.ToString());
System.Threading.Thread.Sleep(1000);
counter++;
}
}
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) =>
textBox1.Text = (string)e.UserState;
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
If (Not BackgroundWorker1.IsBusy) Then
BackgroundWorker1.RunWorkerAsync()
End If
End Sub
Private Sub BackgroundWorker1_DoWork(sender As Object, e As ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Dim counter = 0
Dim max = 10
While counter <= max
BackgroundWorker1.ReportProgress(0, counter.ToString())
System.Threading.Thread.Sleep(1000)
counter += 1
End While
End Sub
Private Sub BackgroundWorker1_ProgressChanged(sender As Object, e As ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
TextBox1.Text = e.UserState
End Sub
.NET Desktop feedback