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:

  1. The System.Windows.Forms.Control.Invoke method, which calls a delegate from the main thread to call the control.
  2. 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