Threading in Windows Forms

One of the issues which frequently comes up in newsgroups is how to handle threading in a UI. There are two golden rules for Windows Forms:

1) Never invoke any method or property on a control created on another thread other than Invoke, BeginInvoke, EndInvoke or CreateGraphics, and InvokeRequired.
Each control is effectively bound to a thread which runs its message pump. If you try to access or change anything in the UI (for example changing the Text property) from a different thread, you run a risk of your program hanging or misbehaving in other ways. You may get away with it in some cases, but only by blind luck. Fortunately, the Invoke, BeginInvoke and EndInvoke methods have been provided so that you can ask the UI thread to call a method for you in a safe manner.
2) Never execute a long-running piece of code in the UI thread.
If your code is running in the UI thread, that means no other code is running in that thread. That means you won't receive events, your controls won't be repainted, etc. This is a very Bad Thing. You can execute long-running code and periodically call Application.DoEvents(), and this is the natural thing for many VB programmers to wish to do - but I'd advise against it. It means you have to consider re-entrancy issues etc, which I believe are harder to diagnose and fix than "normal" threading problems. You have to judge when to call DoEvents, and you can't use anything which might block (network access, for instance) without risking an unresponsive UI. I believe there are message pumping issues in terms of COM objects as well, but I don't have details of them (and I frankly wouldn't understand them fully anyway).

So, if you have a piece of long-running code which you need to execute, you need to create a new thread (or use a thread pool thread if you prefer) to execute it on, and make sure it doesn't directly try to update the UI with its results. The thread creation part is the same as any other threading problem, and we've addressed that before. The interesting bit is going the other way - invoking a method on the UI thread in order to update the UI.

There are two different ways of invoking a method on the UI thread, one synchronous (Invoke) and one asynchronous (BeginInvoke). They work in much the same way - you specify a delegate and (optionally) some arguments, and a message goes on the queue for the UI thread to process. If you use Invoke, the current thread will block until the delegate has been executed. If you use BeginInvoke, the call will return immediately. If you need to get the return value of a delegate invoked asynchronously, you can use EndInvoke with the IAsyncResult returned by BeginInvoke to wait until the delegate has completed and fetch the return value.

There are two options when working out how to get information between the various threads involved. The first option is to have state in the class itself, setting it in one thread, retrieving and processing it in the other (updating the display in the UI thread, for example). The second option is to pass the information as parameters in the delegate. Using state somewhere is necessary if you're creating a new thread rather than using the thread pool - but that doesn't mean you have to use state to return information to the UI. On the other hand, creating a delegate with lots of parameters often feels clumsy, and is in some ways less efficient than using a simple MethodInvoker or EventHandler delegate. These two delegates are treated in a special (fast) manner by Invoke and BeginInvoke. MethodInvoker is just a delegate which takes no parameters and returns no value (like ThreadStart), and EventHandler takes two parameters (a sender and an EventArgs parameter and returns no value. Note, however, that if you pass an EventHandler delegate to Invoke or BeginInvoke then even if you specify parameters yourself, they are ignored - when the method is invoked, the sender will be the control you have invoked it with, and the EventArgs will be EventArgs.Empty.

Here is an example which shows several of the above concepts. Notes are provided after the code.

using System;
using System.Threading;
using System.Windows.Forms;
using System.Drawing;

public class Test : Form
{
    delegate void StringParameterDelegate (string value);
    Label statusIndicator;
    Label counter;
    Button button;
    
    /// <summary>
    /// Lock around target and currentCount
    /// </summary>
    readonly object stateLock = new object();
    int target;
    int currentCount;
    
    Random rng = new Random();
    
    Test()
    {
        Size = new Size (180, 120);
        Text = "Test";
        
        Label lbl = new Label();
        lbl.Text = "Status:";
        lbl.Size = new Size (50, 20);
        lbl.Location = new Point (10, 10);
        Controls.Add(lbl);
        
        lbl = new Label();
        lbl.Text = "Count:";
        lbl.Size = new Size (50, 20);
        lbl.Location = new Point (10, 34);
        Controls.Add(lbl);
        
        statusIndicator = new Label();
        statusIndicator.Size = new Size (100, 20);
        statusIndicator.Location = new Point (70, 10);
        Controls.Add(statusIndicator);

        counter = new Label();
        counter.Size = new Size (100, 20);
        counter.Location = new Point (70, 34);
        Controls.Add(counter);
        
        button = new Button();
        button.Text = "Go";
        button.Size = new Size (50, 20);
        button.Location = new Point (10, 58);
        Controls.Add(button);
        button.Click += new EventHandler (StartThread);
    }
    
    void StartThread (object sender, EventArgs e)
    {
        button.Enabled = false;
        lock (stateLock)
        {
            target = rng.Next(100);
        }
        Thread t = new Thread(new ThreadStart(ThreadJob));
        t.IsBackground = true;
        t.Start();
    }
    
    void ThreadJob()
    {
        MethodInvoker updateCounterDelegate = new MethodInvoker(UpdateCount);
        int localTarget;
        lock (stateLock)
        {
            localTarget = target;
        }
        UpdateStatus("Starting");
        
        lock (stateLock)
        {
            currentCount = 0;
        }
        Invoke (updateCounterDelegate);
        // Pause before starting
        Thread.Sleep(500);
        UpdateStatus("Counting");
        for (int i=0; i < localTarget; i++)
        {
            lock (stateLock)
            {
                currentCount = i;
            }
            // Synchronously show the counter
            Invoke (updateCounterDelegate);
            Thread.Sleep(100);
        }
        UpdateStatus("Finished");
        Invoke (new MethodInvoker(EnableButton));
    }
    
    void UpdateStatus(string value)
    {
        if (InvokeRequired)
        {
            // We're not in the UI thread, so we need to call BeginInvoke
            BeginInvoke(new StringParameterDelegate(UpdateStatus), new object[]{value});
            return;
        }
        // Must be on the UI thread if we've got this far
        statusIndicator.Text = value;
    }
    
    void UpdateCount()
    {
        int tmpCount;
        lock (stateLock)
        {
            tmpCount = currentCount;
        }
        counter.Text = tmpCount.ToString();
    }
    
    void EnableButton()
    {
        button.Enabled = true;
    }

    static void Main()
    {
        Application.Run (new Test());
    }
}

Notes:


Next page: The Thread Pool and Asynchronous Methods
Previous page: Volatility, Atomicity and Interlocking


Back to the main C# page.