Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
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 button2_Click(object sender, EventArgs e)
{
WriteTextUnsafe("Writing message #1 (UI THREAD)");
_ = Task.Run(() => WriteTextUnsafe("Writing message #2 (OTHER THREAD)"));
}
private void WriteTextUnsafe(string text) =>
textBox1.Text += $"{Environment.NewLine}{text}";
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
WriteTextUnsafe("Writing message #1 (UI THREAD)")
Task.Run(Sub() WriteTextUnsafe("Writing message #2 (OTHER THREAD)"))
End Sub
Private Sub WriteTextUnsafe(text As String)
TextBox1.Text += $"{Environment.NewLine}{text}"
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 might 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
Windows Forms applications follow a strict contract-like framework, similar all other Windows UI frameworks: all controls must be created and accessed from the same thread. This is important because Windows requires applications to provide a single dedicated thread to deliver system messages to. Whenever the Windows Window Manager detects an interaction to an application window, such as a key press, a mouse click, or resizing the window, it routes that information to the thread that created and manages the UI, and turns it into actionable events. This thread is known as the UI thread.
Because code running on another thread can't access controls created and managed by the UI thread, Windows Forms provides ways to safely work with these controls from another thread, as demonstrated in the following code examples:
Example: Use Control.InvokeAsync (.NET 9 and later)
The Control.InvokeAsync method (.NET 9+), which provides async-friendly marshaling to the UI thread.
Example: Use the Control.Invoke method:
The Control.Invoke method, which calls a delegate from the main thread to call the control.
Example: Use a BackgroundWorker
A BackgroundWorker component, which offers an event-driven model.
Example: Use Control.InvokeAsync (.NET 9 and later)
Starting with .NET 9, Windows Forms includes the InvokeAsync method, which provides async-friendly marshaling to the UI thread. This method is useful for async event handlers and eliminates many common deadlock scenarios.
Note
Control.InvokeAsync
is only available in .NET 9 and later. It isn't supported in .NET Framework.
Understanding the difference: Invoke vs InvokeAsync
Control.Invoke (Sending - Blocking):
- Synchronously sends the delegate to the UI thread's message queue.
- The calling thread waits until the UI thread processes the delegate.
- Can lead to UI freezes when the delegate marshaled to the message queue is itself waiting for a message to arrive (deadlock).
- Useful when you have results ready to display on the UI thread, for example: disabling a button or setting the text of a control.
Control.InvokeAsync (Posting - Non-blocking):
- Asynchronously posts the delegate to the UI thread's message queue instead of waiting for the invoke to finish.
- Returns immediately without blocking the calling thread.
- Returns a
Task
that can be awaited for completion. - Ideal for async scenarios and prevents UI thread bottlenecks.
Advantages of InvokeAsync
Control.InvokeAsync
has several advantages over the older Control.Invoke
method. It returns a Task
that you can await, making it work well with async and await code. It also prevents common deadlock problems that can happen when mixing async code with synchronous invoke calls. Unlike Control.Invoke
, the InvokeAsync
method doesn't block the calling thread, which keeps your apps responsive.
The method supports cancellation through CancellationToken
, so you can cancel operations when needed. It also handles exceptions properly, passing them back to your code so you can deal with errors appropriately. .NET 9 includes compiler warnings (WFO2001) that help you use the method correctly.
For comprehensive guidance on async event handlers and best practices, see Events overview.
Choosing the right InvokeAsync overload
Control.InvokeAsync
provides four overloads for different scenarios:
Overload | Use Case | Example |
---|---|---|
InvokeAsync(Action) |
Sync operation, no return value. | Update control properties. |
InvokeAsync<T>(Func<T>) |
Sync operation, with return value. | Get control state. |
InvokeAsync(Func<CancellationToken, ValueTask>) |
Async operation, no return value.* | Long-running UI updates. |
InvokeAsync<T>(Func<CancellationToken, ValueTask<T>>) |
Async operation, with return value.* | Async data fetching with result. |
*Visual Basic doesn't support awaiting a ValueTask.
The following example demonstrates using InvokeAsync
to safely update controls from a background thread:
private async void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;
try
{
// Perform background work
await Task.Run(async () =>
{
for (int i = 0; i <= 100; i += 10)
{
// Simulate work
await Task.Delay(100);
// Create local variable to avoid closure issues
int currentProgress = i;
// Update UI safely from background thread
await loggingTextBox.InvokeAsync(() =>
{
loggingTextBox.Text = $"Progress: {currentProgress}%";
});
}
});
loggingTextBox.Text = "Operation completed!";
}
finally
{
button1.Enabled = true;
}
}
Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles button1.Click
button1.Enabled = False
Try
' Perform background work
Await Task.Run(Async Function()
For i As Integer = 0 To 100 Step 10
' Simulate work
Await Task.Delay(100)
' Create local variable to avoid closure issues
Dim currentProgress As Integer = i
' Update UI safely from background thread
Await loggingTextBox.InvokeAsync(Sub()
loggingTextBox.Text = $"Progress: {currentProgress}%"
End Sub)
Next
End Function)
' Update UI after completion
Await loggingTextBox.InvokeAsync(Sub()
loggingTextBox.Text = "Operation completed!"
End Sub)
Finally
button1.Enabled = True
End Try
End Sub
For async operations that need to run on the UI thread, use the async overload:
private async void button2_Click(object sender, EventArgs e)
{
button2.Enabled = false;
try
{
loggingTextBox.Text = "Starting operation...";
// Dispatch and run on a new thread, but wait for tasks to finish
// Exceptions are rethrown here, because await is used
await Task.WhenAll(Task.Run(SomeApiCallAsync),
Task.Run(SomeApiCallAsync),
Task.Run(SomeApiCallAsync));
// Dispatch and run on a new thread, but don't wait for task to finish
// Exceptions are not rethrown here, because await is not used
_ = Task.Run(SomeApiCallAsync);
}
catch (OperationCanceledException)
{
loggingTextBox.Text += "Operation canceled.";
}
catch (Exception ex)
{
loggingTextBox.Text += $"Error: {ex.Message}";
}
finally
{
button2.Enabled = true;
}
}
private async Task SomeApiCallAsync()
{
using var client = new HttpClient();
// Simulate random network delay
await Task.Delay(Random.Shared.Next(500, 2500));
// Do I/O asynchronously
string result = await client.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md");
// Marshal back to UI thread
await this.InvokeAsync(async (cancelToken) =>
{
loggingTextBox.Text += $"{Environment.NewLine}Operation finished at: {DateTime.Now:HH:mm:ss.fff}";
});
// Do more async I/O ...
}
Private Async Sub Button2_Click(sender As Object, e As EventArgs) Handles button2.Click
button2.Enabled = False
Try
loggingTextBox.Text = "Starting operation..."
' Dispatch and run on a new thread, but wait for tasks to finish
' Exceptions are rethrown here, because await is used
Await Task.WhenAll(Task.Run(AddressOf SomeApiCallAsync),
Task.Run(AddressOf SomeApiCallAsync),
Task.Run(AddressOf SomeApiCallAsync))
' Dispatch and run on a new thread, but don't wait for task to finish
' Exceptions are not rethrown here, because await is not used
Call Task.Run(AddressOf SomeApiCallAsync)
Catch ex As OperationCanceledException
loggingTextBox.Text += "Operation canceled."
Catch ex As Exception
loggingTextBox.Text += $"Error: {ex.Message}"
Finally
button2.Enabled = True
End Try
End Sub
Private Async Function SomeApiCallAsync() As Task
Using client As New HttpClient()
' Simulate random network delay
Await Task.Delay(Random.Shared.Next(500, 2500))
' Do I/O asynchronously
Dim result As String = Await client.GetStringAsync("https://github.com/dotnet/docs/raw/refs/heads/main/README.md")
' Marshal back to UI thread
' Extra work here in VB to handle ValueTask conversion
Await Me.InvokeAsync(DirectCast(
Async Function(cancelToken As CancellationToken) As Task
loggingTextBox.Text &= $"{Environment.NewLine}Operation finished at: {DateTime.Now:HH:mm:ss.fff}"
End Function,
Func(Of CancellationToken, Task)).AsValueTask() 'Extension method to convert Task
)
' Do more Async I/O ...
End Using
End Function
Note
If you're using Visual Basic, the previous code snippet used an extension method to convert a ValueTask to a Task. The extension method code is available on GitHub.
Example: Use the Control.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)
{
WriteTextSafe("Writing message #1");
_ = Task.Run(() => WriteTextSafe("Writing message #2"));
}
public void WriteTextSafe(string text)
{
if (textBox1.InvokeRequired)
textBox1.Invoke(() => WriteTextSafe($"{text} (NON-UI THREAD)"));
else
textBox1.Text += $"{Environment.NewLine}{text}";
}
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
WriteTextSafe("Writing message #1")
Task.Run(Sub() WriteTextSafe("Writing message #2"))
End Sub
Private Sub WriteTextSafe(text As String)
If (TextBox1.InvokeRequired) Then
TextBox1.Invoke(Sub()
WriteTextSafe($"{text} (NON-UI THREAD)")
End Sub)
Else
TextBox1.Text += $"{Environment.NewLine}{text}"
End If
End Sub
For more information on how Invoke
differs from InvokeAsync
, see Understanding the difference: Invoke vs InvokeAsync.
Example: Use a BackgroundWorker
An easy way to implement multi-threading scenarios while guaranteeing that the access to a control or form is performed only on the main thread (UI thread), 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.
Important
The BackgroundWorker
component is no longer the recommended approach for asynchronous scenarios in Windows Forms applications. While we continue supporting this component for backwards compatibility, it only addresses offloading processor workload from the UI thread to another thread. It doesn't handle other asynchronous scenarios like file I/O or network operations where the processor might not be actively working.
For modern asynchronous programming, use async
methods with await
instead. If you need to explicitly offload processor-intensive work, use Task.Run
to create and start a new task, which you can then await like any other asynchronous operation. For more information, see Example: Use Control.InvokeAsync (.NET 9 and later) and Cross-thread operations and events.
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. 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(); // Not awaitable
}
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() ' Not awaitable
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