Share via


New information has been added to this article since publication.
Refer to the Editor's Update below.

Windows Forms

Give Your .NET-based Application a Fast and Responsive UI with Multiple Threads

Ian Griffiths

Code download available at:Multithreading.exe(106 KB)

This article assumes you're familiar with .NET, C#, and Windows Forms

Level of Difficulty123

SUMMARY

If your application performs any non-UI processing on the thread that controls the user interface, it may make the app seem slow and sluggish, frustrating users. But writing multithreaded apps for Windows has long been restricted to C++ developers. Now with the .NET Framework, you can take advantage of multiple threads in C# to control the flow of instructions in your programs and isolate the UI thread for a fast and snappy user interface. This article shows you how. Also, it discusses the pitfalls of multiple threads and presents a framework for safe concurrent thread execution.

Contents

Why Multiple Threads?
Asynchronous Delegate Invocation
Threads and Controls
Calling a Control on the Right Thread
Wrapping Control.Invoke
Locking
Deadlock
Keep it Simple
Cancellation
Program Shutdown
Error Handling
Conclusion

U sers hate slow programs. The more sluggish a program is, the less a user will like it. Judicious use of multiple threads can improve the responsiveness of a program's UI when performing lengthy operations, making everything seem faster. Multithreaded programming in Windows® was once the domain of C++ developers, but now it is available in all Microsoft® .NET-compliant languages, including Visual Basic® .NET. Yet Windows Forms imposes some significant restrictions on the use of threads. This article explains those restrictions and how to work with them to provide a snappy, high-quality UI experience even when the program is performing tasks that are inherently slow.

Why Multiple Threads?

Multithreaded programs are much harder to write than single-threaded ones, and the indiscriminate use of threads is a big reason subtle bugs can take weeks to locate. This raises two obvious questions: why not stick to single-threaded code? And if you must use multiple threads, how can you avoid the pitfalls? Most of this article answers the second question, but first I will explain why you really need multiple threads.

Multithreading can enable you to keep the UI responsive by ensuring that the program never "goes to sleep" on you. Most programs have moments where they pay no attention to the user: they are too busy doing some work on your behalf to respond to further requests. Perhaps the most widely known example of this is the combobox that appears at the top of the Open File dialog. If you happen to have a disk in your CD-ROM drive when you expand this combobox, the computer will often spin this disk before showing the list. This can take several seconds, during which time the program will not respond to any input nor let you cancel the operation, which is particularly infuriating if you didn't actually plan to use the CD-ROM.

The reason that the UI freezes during such operations is that in a single-threaded program, it is not possible for the single thread to wait for the CD-ROM drive to spin up while simultaneously handling incoming user input, as Figure 1 illustrates. The Open File dialog will be calling certain blocking APIs to determine the title of the CD-ROM. A blocking API is one which does not return until it has completed its work and therefore prevents the thread from doing anything else in the meantime.

Figure 1 Single Thread

Figure 1** Single Thread **

With multithreading, long-winded tasks like this can be run on their own thread, often called a worker thread. Blocking operations no longer cause the user interface to freeze because only the worker thread is blocked, as you can see in Figure 2. The application's main thread can continue dealing with the user's mouse and keyboard input while the other thread is blocked waiting for the CD-ROM to spin up, or for whatever else the worker thread may be doing.

Figure 2 Multiple Threads

Figure 2** Multiple Threads **

The general principle is that the thread that's responsible for responding to the user and keeping the user interface up to date (usually referred to as the UI thread) should never be used to perform any lengthy operation. As a rule of thumb, anything that could take longer than about 30ms is a candidate for removal from the UI thread. This is a little aggressive—30ms is about the shortest interval that most people will perceive as being anything other than instantaneous and it's actually slightly less than the interval between successive frames shown on a movie screen.

A delay of more than 30ms between a mouse click and the corresponding UI cue (such as a button being redrawn) will be perceptible as a slight disconnection between the action and the display and will be disconcerting in the way that loss of lip-sync on film can be. For absolute top-quality responsiveness, 30ms has to be the limit. If, on the other hand, you really don't mind a slight feeling of disconnection but you don't want a hiatus to be long enough to actively annoy the user, then 100ms is widely thought to be about as much as you can reasonably get away with.

What this means is that any blocking operation that either has to wait for something mechanical to happen (waiting for the CD-ROM to start or the hard disk to locate the data, for example) or for a response to come in over the network should be performed on a worker thread if you want the user interface to remain responsive.

Asynchronous Delegate Invocation

The simplest way to run code on a worker thread is to use asynchronous delegate invocation (all delegates provide this facility). Delegates are normally invoked synchronously—that is, when you call through a delegate, the call does not return until the wrapped method returns. To invoke a delegate asynchronously, call the BeginInvoke method, which will queue the method to be run on a thread from the system thread pool. The calling thread returns immediately without waiting for the method to complete. This makes it ideal for UI programs because you can use it in order to launch lengthy jobs without slowing down the user interface.

For example, in the following code, the System.Windows.Forms.MethodInvoker type is a system-defined delegate for calling methods that take no parameters.

private void StartSomeWorkFromUIThread () { // The work we want to do is too slow for the UI // thread, so let's farm it out to a worker thread. MethodInvoker mi = new MethodInvoker( RunsOnWorkerThread); mi.BeginInvoke(null, null); // This will not block. } // The slow work is done here, on a thread // from the system thread pool. private void RunsOnWorkerThread() { DoSomethingSlow(); }

If you wanted to pass parameters, you would either select an appropriate system-defined delegate type or define your own. There is nothing magic about the MethodInvoker delegate. As with any delegate, calling BeginInvoke will cause the method to be run on a thread from the system thread pool, leaving the UI thread free to do other work. In this case the method returns no data, so you can just launch it and let it do its stuff. If you cared about any of the results from the method, then the return value from BeginInvoke would have been significant, and you would probably not be passing null parameters. However, for most UI applications, this "launch and leave" style turns out to be the most effective for reasons that will be discussed shortly. You should note that BeginInvoke returns an IAsyncResult. This can be used in conjunction with the delegate's EndInvoke method to retrieve the results of the method call once it has completed.

Other techniques for running methods on other threads are available, such as using the thread pool API directly or creating your own thread. For the majority of user interface applications, however, asynchronous delegate invocation will suffice. Not only does this make for easier code, it also avoids creating more threads than is strictly necessary because you will be using the shared threads in the thread pool, improving your application's overall performance.

Threads and Controls

The Windows Forms architecture has strict rules about the use of threads. If you are writing only single-threaded app, it is safe to know absolutely nothing about these rules because it is not possible for single-threaded code to break them. As soon as you introduce multiple threads, however, you need to understand the single most important rule of threading in Windows Forms: with very few exceptions, you must never use any member of a Control on any thread other than the one that created it.

There are documented exceptions to this rule, but only a few. This applies to any object whose class derives from System.Windows.Forms.Control, which includes more or less everything in the UI. All UI elements, including forms themselves, are objects that derive from the Control class. Furthermore, the upshot of this rule is that a contained Control, such as a button contained by a form, must live on the same thread as its containing control. That is, all of the controls on a window belong to the same UI thread. In practice, most Windows Forms applications end up having a single thread on which all UI activity happens. This thread is usually referred to as the UI thread. This means that you cannot call any methods on anything in the user interface unless the documentation for that method says that it's OK. The exceptions to the rule, which are always documented, are few and far between. Note that the following code is illegal:

// Created on UI thread private Label lblStatus; ••• // Doesn't run on UI thread private void RunsOnWorkerThread() { DoSomethingSlow(); lblStatus.Text = "Finished!"; // BAD!! }

If you try this with version 1.0 of the .NET Framework, you will probably get away with it—or so it will seem at first. This is the main problem with multithreading bugs—they tend not to show themselves immediately. Even when you do something wrong, everything appears to work right up until the first time you demo your program. But make no mistake—the code I just showed you definitely breaks the rules, and anyone relying on the "well, it worked when I tried it" style of proof can look forward to some painful debugging sessions in the near future.

Be aware that this problem can arise without your ever having created a thread explicitly. Any code which uses a delegate's asynchronous invocation facility (calling its BeginInvoke method) can run into the same problem. Delegates provide a very attractive solution for dealing with slow, blocking operations in UI applications because they make it very easy for you to get such work off the UI thread without having to create a new thread yourself. But since code run with asynchronous delegate invocation will run on a thread from the thread pool, it cannot access any UI elements. The same restrictions apply to both threads in the thread pool and to manually created worker threads.

Calling a Control on the Right Thread

The restrictions on controls seem disastrous for multithreaded programs. How can some slow operation running on a worker thread keep the user informed of its progress if it can't do anything to the UI? Indeed, how will the user even find out when the work is complete or if an error occurs? Fortunately, although this restriction is inconvenient, it is not insurmountable. There are a variety of ways to get a message from a worker thread to the UI thread. In theory, you could produce your own mechanism using low-level synchronization primitives and polling, but fortunately there is no need to resort to this type of low-level work because a solution exists in the form of the Control class's Invoke method.

The Invoke method is one of the Control class's few documented exceptions to the threading rule: it is always OK to call Invoke on a Control from any thread. The Invoke method itself simply takes a delegate and an optional parameter list and calls the delegate for you on the UI thread, regardless of which thread issued the Invoke call. In fact, it is simple to get any method to run on the right thread for the control. You should note, though, that this mechanism only works if the UI thread is not currently blocked—invocations can only get through when the UI thread is ready to handle user input. This is another good reason never to block the UI thread. The Invoke method tests to see whether the calling thread is the UI thread. If so, it calls the delegate directly. Otherwise, it arranges for a thread switch and calls the delegate on the UI thread. In either case, the method wrapped by the delegate will run on the UI thread, and Invoke will not return until the method has completed.

The Control class also supplies an asynchronous version of Invoke, which returns immediately and arranges for the method to run on the UI thread at some point in the future. This is called BeginInvoke and looks very similar to asynchronous delegate invocation, the obvious difference being that with a delegate, the call runs asynchronously on some thread in the thread pool, whereas here it runs asynchronously on the UI thread. In fact, the Control's Invoke, BeginInvoke, and EndInvoke methods, as well as the InvokeRequired property, are all members of the ISynchronizeInvoke interface. This interface can be implemented by any class that needs to control the way events are delivered to it.

You should use BeginInvoke rather than Invoke wherever possible because it is less prone to deadlock. Because Invoke is synchronous, it will block the worker thread until the UI thread becomes available. But what if the UI thread was waiting for the worker thread to do something? The application will deadlock. BeginInvoke avoids this since it never waits for the UI thread.

Now I'll review a legal version of the code snippet I presented earlier. First, I must pass a delegate to the Control's BeginInvoke method so that I can run my thread-sensitive code on the UI thread. This means placing that code in its own method, as shown in Figure 3. Once the worker thread has finished the slow work, it calls BeginInvoke on the Label in order to run some code on its UI thread. From there, it can update the user interface.

Figure 3 Proper Thread Use

// Created on UI thread private Label lblStatus; ••• // Doesn't run on UI thread private void RunsOnWorkerThread() { DoSomethingSlow(); // Do UI update on UI thread object[] pList = { this, System.EventArgs.Empty }; lblStatus.BeginInvoke( new System.EventHandler(UpdateUI), pList); } ••• // Code to be run back on the UI thread // (using System.EventHandler signature // so we don't need to define a new // delegate type here) private void UpdateUI(object o, System.EventArgs e) { // Now OK - this method will be called via // Control.Invoke, so we are allowed to do // things to the UI. lblStatus.Text = "Finished!"; }

Wrapping Control.Invoke

[Editor's Update - 4/27/2005: The code in Figure 4 has been updated. When passing a System.EventHandler to Control.Invoke or Control.BeginInvoke, EventArgs.Empty will be used as the EventArgs argument regardless of the actual EventArgs value specified in the arguments array.]

Figure 4 Wrapper Functions

public class MyForm : System.Windows.Forms.Form { ... public void ShowProgress(string msg, int percentDone) { // Wrap the parameters in some EventArgs-derived custom class: System.EventArgs e = new MyProgressEvents(msg, percentDone); object[] pList = { this, e }; // Invoke the method. This class is derived // from Form, so we can just call BeginInvoke // to get to the UI thread. BeginInvoke(new MyProgressEventsHandler(UpdateUI), pList); } private delegate void MyProgressEventsHandler( object sender, MyProgressEvents e); private void UpdateUI(object sender, MyProgressEvents e) { lblStatus.Text = e.Msg; myProgressControl.Value = e.PercentDone; } }

The ShowProgress method encapsulates the work of handing the call over to the correct thread. This means the worker thread code would no longer have to worry itself too much over the UI details—it would just call ShowProgress at regular intervals. Notice that I have defined my own method which is exempt from the "must be called on the UI thread" rule because it, in turn, only calls other methods that are exempt. This technique suggests a more general idiom: why not write public methods on the control which are documented exceptions to the UI thread rule?

It turns out that the Control class provides a useful facility for such methods. If I am providing a public method which is designed to be callable from any thread, it is entirely possible that someone will call this method from the UI thread. In this case, calling BeginInvoke is not necessary because I am already on the right thread. It would be a waste of time and resources to call Invoke instead of calling the appropriate method directly. To help avoid this, the Control class exposes a property called InvokeRequired. This is another exception to the "UI thread only" rule. It can be read from any thread, will return false if the calling thread is the UI thread, and will return true for any other thread. This means I can modify the wrapper in the following way:

public void ShowProgress(string msg, int percentDone) { if (InvokeRequired) { // As before ••• } else { // We're already on the UI thread just // call straight through. UpdateUI(this, new MyProgressEvents(msg, PercentDone)); } }

ShowProgress can now be documented as a public method which can be called from any thread. This doesn't get rid of the complexity—the code to perform BeginInvoke is still here, it's just in one place. Unfortunately, there is no easy way to get rid of it entirely.

Locking

Any concurrent system must deal with the fact that two threads might try to use a single piece of data simultaneously. Sometimes this might not be a problem—if multiple threads attempt to read a field in an object at the same time, no problems will arise. If any thread wants to modify the data, however, you do have a problem. It is possible for the reading threads to see bogus values if their read coincides with a write on another thread. If two threads write to the same place at the same time, it is possible that any thread that reads from this place will see garbage at any time after the simultaneous writes occurred. Though this behavior only arises in certain circumstances, the read doesn't even have to collide with the writes—the data could be a mix of the two writes and would remain bad until the next time a value is written. In order to avoid this problem, you must take steps to ensure that only one thread at a time can read or write an object's state.

The way you usually protect yourself from these problems is to use the runtime's locking facilities. C# lets you exploit these to protect your code with the lock keyword (Visual Basic has a similar construct called SyncLock). The rule is that any object that expects to have its methods called on multiple threads should use a lock construct every time it accesses its fields, whether it is reading or writing them. For example, take a look at Figure 5.

Figure 5 Locking Threads

// This field could be modified and read on any thread, so all access // must be protected by locking the object. private double myPosition; ••• public double Position { get { // Position could be modified from any thread, so we need to lock // this object to make sure we get a consistent value. lock (this) { return myPosition; } } set { lock (this) { myPosition = value; } } } public void MoveBy(double offset) { // Here we are reading, checking and then modifying the value. It is // vitally important that this entire sequence is protected by a // single lock block. lock (this) { double newPos = Position + offset; // Check within range - MINPOS and MAXPOS // will be const values defined somewhere in // this class if (newPos > MAXPOS) newPos = MAXPOS; else if (newPos < MINPOS) newPos = MINPOS; Position = newPos; } }

The way this works is that each object in the common language runtime (CLR) has a lock associated with it which any thread can acquire, but which can only be owned by one thread at a time. If a thread attempts to acquire the lock when another already owns it, it will have to wait until the owning thread releases the lock. The C# lock construct acquires the object lock (waiting for another thread to finish with it, if necessary), and holds it until the code inside the braces exits. The lock will be released if the execution runs to the end of the block, returns from the middle of the block, or throws an exception which is not caught in the block.

Note that the logic in the MoveBy method is protected by a single lock statement. When making modifications that are more complex than a simple read or write, the whole process must be protected by a single lock statement. This also applies when making updates to multiple fields—the lock must not be released until the object is in a consistent state. If the lock is released partway through updating the state, other threads may be able to get in and see this inconsistency. If you already hold a lock and you call a method that attempts to acquire the same lock, this does not cause a problem—a single thread is allowed to acquire the same lock several times over. This is important for code where locking is needed to protect both low-level access to the field and also higher-level operations performed on the field. MoveBy uses the Position property, and they both acquire the lock. The lock is not properly released until the outermost lock block finishes.

You must be thorough with your locking code. All it takes is one omission to spoil things for everyone. If just one method modifies the state without acquiring the object lock, the remaining code that carefully locks the object before using it does so in vain. Likewise, if a thread tries to read the state without first acquiring the lock, it may read incorrect values. The runtime cannot check to make sure that multithreaded code behaves properly.

Deadlock

Locks are essential for ensuring that multithreaded code behaves correctly, even though they can introduce new risks of their own. The easiest way of running code on another thread is to use asynchronous delegate invocation (see Figure 6).

Figure 6 Asynchronous Delegate Invocation

public class Foo { public void CallBar() { lock (this) { Bar myBar = new Bar (); myBar.BarWork(this); } } // This will be called back on a worker thread public void FooWork() { lock (this) { // do some work ••• } } } public class Bar { public void BarWork(Foo myFoo) { // Call Foo on different thread via delegate. MethodInvoker mi = new MethodInvoker( myFoo.FooWork); IAsyncResult ar = mi.BeginInvoke(null, null); // do some work ••• // Now wait for delegate call to complete (DEADLOCK!) mi.EndInvoke(ar); } }

This code will grind to a halt if Foo's CallBar method is ever called. The CallBar method acquires the lock on the Foo object and will not release it until BarWork returns. BarWork then uses asynchronous delegate invocation to call the Foo object's FooWork method on a thread pool thread. It then does some other work before calling the delegate's EndInvoke method. EndInvoke will wait for the worker thread to complete, but the worker thread will be blocked in FooWork. It also tries to acquire the Foo object's lock, but that lock is already held in the CallBar method. So, FooWork is waiting for CallBar to release the lock, but CallBar is waiting for BarWork to return. Unfortunately, BarWork is waiting for FooWork to complete, so FooWork must complete before it can start. As a result, none of the threads can proceed.

This is an example of deadlock, which is where two or more threads are both blocked waiting for the other to proceed. The behavior here is slightly different from the standard deadlock scenario, which usually involves two locks. This illustrates that if a causality (a chain of procedure calls) crosses a thread boundary, you can get a deadlock even when only one lock is involved! This is an important fact given that Control.Invoke is a way of calling a procedure across a thread boundary. BeginInvoke does not suffer from this problem because it doesn't make the causality cross threads. In effect it causes a brand new causality to start on a thread pool thread, allowing the original one to proceed independently. However, if you keep the IAsyncResult that BeginInvoke returns and use this to call EndInvoke, then you are in trouble again because EndInvoke effectively merges the two causalities back into one. The simplest way to avoid this situation is to never wait for a cross-thread call to complete while you are holding an object lock. To ensure this, avoid calling either Invoke or EndInvoke inside a lock statement. As a result, you won't be waiting for some other thread to do something while you are holding an object lock. Sticking to this rule is easier said than done.

When examining the code for BarWork, it is far from obvious that it is scoped by a lock statement because there is no lock statement in this method. The problem arises only because BarWork is called from within a lock statement in the Foo.CallBar method. This means that it is only safe to call Control.Invoke or EndIn-voke if you are sure that there are no functions calling you that own locks! For non-private methods, there is no easy way to guarantee this, so for these, the best rule would be to never call Control.Invoke or EndInvoke at all. This is why the "launch and leave" style of programming is preferable wherever possible and why Control.BeginInvoke is usually a much better solution than Control.Invoke.

Sometimes there is no option but to break this rule, in which case very careful and thorough analysis is the only answer. But if at all possible, you should avoid blocking while holding locks because if you do, deadlock is difficult to eliminate.

Keep it Simple

How can you best reap the benefits of multithreading without suffering from the tricky bugs that can plague concurrent code? If your improved UI responsiveness merely allows the program to crash at a moment's notice, you can hardly claim to have improved the user experience. Most of the problems common in multithreaded code arise from the inherent complexity of having lots of things occurring at once—most people are better at thinking about sequential processes than concurrent ones. Often, the best solution is to keep things as simple as possible.

The nature of UI code is that it receives events from external sources like user input. It handles these as they occur, but it spends most of its time waiting for things to happen. If you can structure the communication between the worker thread and the UI to fit into this model, you are unlikely to see many problems because no new idioms will have been introduced. I am keeping things simple by treating the worker thread as another source of asynchronous events. Just as the Button control delivers events such as Click and MouseEnter, you can treat the worker thread as something that delivers events such as ProgressUpdate and WorkComplete. It's up to you whether to treat this simply as an analogy or to actually encapsulate your worker object in a class which exposes proper events in this way. The latter will probably require more code, but will leave your user interface code looking pleasingly uniform. In either case, you will need Control.BeginInvoke to deliver these events on the correct thread.

For the worker thread, the simplest approach is to write the code as a normal sequential piece of code. But what if you want to use the "worker thread as event source" model I've just described? This model is desirable, but it places a constraint on the interaction this code has with the user interface: the thread can only send information to the UI, not request it.

For example, it would be difficult for your worker thread to bring up a dialog halfway through to request information needed to complete the result. If you do need to do this, it is probably better to open such a dialog on the worker thread, not the main UI thread. This constraint is a good thing, as it ensures a very simple model for communication between the two threads—and simplicity is the key to success here. The great thing about this style of development is that neither thread is ever blocking while waiting for the other one. This is a good strategy for avoiding deadlock.

Figure 7 shows the use of asynchronous delegate invocation to perform a potentially slow operation (reading the contents of a directory) on a worker thread, and then displaying the results back in the UI. It doesn't go as far as using the high-level event syntax, but it does handle the completion of the worker code in much the same way that it would handle events such as clicks.

Figure 7 A Slow Operation on a Worker Thread

private void btnRead_Click(object o, EventArgs e) { btnRead.Enabled = false; txtDir.Enabled = false; Cursor = Cursors.AppStarting; // Clear the list view lvwDirList.Clear(); // This could be slow, so do this via an async delegate. ReadDelegate dlg = new ReadDelegate(ReadDirectory); dlg.BeginInvoke(txtDir.Text, null, null); } // Worker function (and associated delegate) private delegate void ReadDelegate(string dirName); private void ReadDirectory(string dirName) { try { string[] names = Directory.GetFileSystemEntries(dirName); // Marshal the results back onto the right thread. object[] pList = { names }; BeginInvoke(new DirRead(readDir_DirEntries), pList); } catch (DirectoryNotFoundException) { // Raise the directory not found 'event' BeginInvoke(new MethodInvoker(readDir_DirNotFound)); } catch (Exception e) { // Raise the miscellaneous error 'event'. object[] pList = { e }; BeginInvoke(new DirError(readDir_Error), pList); } } // Functions called via Invoke by worker thread. private delegate void DirRead(string[] names); private void readDir_DirEntries(string[] names) { Cursor = Cursors.WaitCursor; foreach(string name in names) { lvwDirList.Items.Add(name); } ResetCtls(); } private void readDir_DirNotFound() { MessageBox.Show(this, "Directory not found", "Error"); ResetCtls(); } private delegate void DirError(Exception e); private void readDir_Error(Exception e) { MessageBox.Show(this, string.Format("Error reading directory:\n{0}", e.Message), "Error"); ResetCtls(); } // Set the relevant controls and cursors back to their default // 'ready' states after the operation has completed. private void ResetCtls() { // Re-enable the Read button and put the cursor back to normal. btnRead.Enabled = true; txtDir.Enabled = true; Cursor = Cursors.Default; }

Cancellation

The problem with the previous example is that the only way to cancel the operation is to quit the entire application. Although the UI remains responsive while reading a directory, the user cannot view another directory, since the program disables the relevant buttons until the current operation completes. It would be unfortunate if you attempted to read a directory on a remote machine that turned out to be unresponsive, since it can take a long time for such an operation to time out.

The ability to cancel an operation can be hard to arrange, although it depends on precisely what is meant by cancel. One possible interpretation is "stop waiting for this operation to complete and let me get on with a different operation." This really amounts to abandoning the operation in progress and ignoring whatever results come in when it does eventually complete. For the present example, this is the best you can do, since the operation in question (reading the contents of a directory) is done with a single blocking API call for which there is no means of cancellation. But even this fairly loose pseudo-cancelling requires a certain amount of effort on your part. If you just decided to launch a new read without waiting for the old one to complete, there would be no way of knowing which of the two outstanding requests the next notification you received came from.

The only way to support cancellation of requests running on a worker thread is to provide some kind of call object associated with each request. At its simplest, this would just act as a cookie, which the worker thread passes back in with each notification, allowing the UI thread to correlate events with requests. Through a simple identity comparison (see Figure 8), the UI code can work out whether an event was from the current request or one that has long since been abandoned.

Figure 8 Which Request is This?

// Member variable holding the current request—gets updated with the // current call object every time we start a new operation. (This call // object would also be made available to the worker function—either as a // parameter, or possibly by making the worker function a member of the // call object.) private object currReq; ••• private void workerNotify(object callObj) { // Only process this event if it's from the last request we made. // (I.e. ignore belated notifications from requests we've long since // abandoned). if (object.ReferenceEquals(callObj, currReq)) { // This is for an operation we still care about, so handle it ••• } }

This is all very well if simple abandonment is sufficient, but you might want to do better. If the worker thread is performing a complex operation that consists of a series of blocking operations in a row, you would probably want the worker thread to stop at the earliest opportunity. Otherwise, it could be continuing for minutes with work that is no longer needed. In this case, the call object will need to do a little more than just act as a passive cookie. At the very least, it will need to maintain a flag indicating whether the request has been cancelled. The UI could set this flag at any point, and the worker thread code would test the flag regularly during its execution to see if it should abandon its work.

Given this approach, there are some decisions to make: if the UI cancels the operation, does it then wait until the worker thread notices the cancellation? If not, there is a race condition you need to consider: it is possible that the UI thread might cancel the operation, but before being able to set the control flag the worker thread had already decided to deliver a notification. Because the UI thread decided not to wait until the worker thread dealt with the cancellation, it is possible that the UI thread will continue to receive notifications from the worker thread. It might even receive several if the worker thread was delivering them asynchronously using BeginInvoke. The UI thread can always deal with this in the same way as the "abandoning" approach—by checking the identity of the call object and ignoring notifications for operations it no longer cares about. Alternatively, I could try to fix this by using locking in the call object and never calling BeginInvoke from the worker thread. But since it is simpler for the UI thread to simply check that it's still interested in an event before handling it, you are likely to have fewer problems using that approach.

See the code download (at the link at the top of this article) for AsyncUtils, a useful base class that provides cancellation functionality for worker thread-based operations. Figure 9 shows a derived class that implements a recursive directory search with cancellation support. These classes illustrate some interesting techniques. They both use the C# event syntax to provide notifications. The base class exposes events that are raised when the operation completes successfully, when it is cancelled, and when it throws an exception. The derived class extends this with events that notify the client of search matches and also of its progress, showing which directory it is on at the moment. These events are always delivered on the UI thread. In fact, these classes are not restricted to the Control class—they can deliver events to anything that implements the ISynchronizeInvoke interface. Figure 10 is an example Windows Forms application which provides a user interface to the search class. It allows searches to be cancelled and shows their progress and results.

Figure 10 A Search UI

public class TestHarness : System.Windows.Forms.Form { private DirSearchWorker currSearch; //. . . Usual Windows Forms boilerplate . . . private void btnSearch_Click(object sender, EventArgs e) { if (currSearch == null) { lstResults.Items.Clear(); currSearch = new DirSearchWorker(this, txtDir.Text, txtRegExp.Text); currSearch.Completed += new EventHandler(Search_Finished); currSearch.Cancelled += new EventHandler(Search_Cancelled); currSearch.Match += new DirSearchWorker.MatchEvent(Search_Result); currSearch.Dir += new DirSearchWorker.MatchEvent(Search_Dir); currSearch.Failed += new System.Threading.ThreadExceptionEventHandler(Search_Failed); btnSearch.Text = "&Cancel"; lblResult.Text = "Searching..."; Cursor = Cursors.AppStarting; currSearch.Start(); } else { lblResult.Text = "Cancelling..."; lblResult.Update(); // Doing synchronous cancel, so the UI will be inactive // until it completes. For certain weird cases like the // Internet Explorer cache this can take several seconds, so // bring up the wait cursor. Cursor = Cursors.WaitCursor; currSearch.CancelAndWait(); Cursor = Cursors.Default; } } private void btnExit_Click(object sender, System.EventArgs e) { if (currSearch != null) { currSearch.CancelAndWait(); } Close(); } private void Search_Finished(object sender, EventArgs e) { ResetCtls(); lblResult.Text = "Finished"; } private void Search_Cancelled(object sender, EventArgs e) { ResetCtls(); lblResult.Text = "Cancelled"; } private void Search_Failed(object sender, System.Threading.ThreadExceptionEventArgs e) { ResetCtls(); lblResult.Text = string.Format("Error ({0}): {1}", e.Exception.GetType().ToString(), e.Exception.Message); } private void ResetCtls() { btnSearch.Text = "&Search"; currSearch = null; Cursor = Cursors.Default; } private void Search_Result(string name) { lstResults.Items.Add(name); } private void Search_Dir(string name)

Figure 9 A Cancellable Search

public sealed class DirSearchWorker : AsyncOperation { public delegate void MatchEvent(string name); public event MatchEvent Match; public event MatchEvent Dir; private string searchDir; private Regex searchExpr; public DirSearchWorker(ISynchronizeInvoke isi, string dir, string regexp) : base(isi) { searchDir = dir; searchExpr = new Regex(regexp, RegexOptions.Compiled); } protected override void DoWork() { DoSearch(searchDir); // When a cancel occurs, the recursive DoSearch drops back // here asap, so we'd better acknowledge cancellation. if (CancelRequested) { AcknowledgeCancel(); } } private void DoSearch(string dir) { // Stop if cancellation was requested if (!CancelRequested) { OnDir(dir); foreach(string name in Directory.GetFileSystemEntries(dir)) { // Check for cancellation here too, otherwise for really // large directories (such as those in the Internet // Explorer cache) the user might have to wait for // several seconds before the cancellation happens. if (CancelRequested) return; if (searchExpr.IsMatch(name)) { OnMatch(name); } } foreach(string name in Directory.GetDirectories(dir)) { DoSearch(name); } } } private void OnMatch(string name) { lock(this) { FireAsync(Match, name); } } private void OnDir(string name) { lock(this) { FireAsync(Dir, name); } } }

Program Shutdown

In some cases, it will be acceptable to "launch and leave" asynchronous operations without the added complexity required to make them cancellable. However, even though your user interface might not require cancellation, it is possible that you will still need to implement this facility to let your program shut down cleanly.

If any worker threads created by the thread pool are still running when your application exits, they will be terminated. This termination is fairly violent—the shutdown will even bypass any finally blocks that are still in scope. If any of your asynchronous operations perform work that should not be interrupted in that way, you must make sure that these operations have completed before shutting down. Such operations might include writing to a file, since the file will probably be corrupted after an abrupt halt.

One way to do this is to create your own thread, instead of one from the worker thread pool, which would of course rule out the use of asynchronous delegate invocation. Then even if the main thread shuts down, the application will not terminate until your thread exits. The System.Threading.Thread class has an IsBackground property which controls this behavior. It is false by default, and the CLR will not normally terminate the application until all non-background threads have exited. This can lead to a different problem, however, since your application may now hang around much longer than you intended. The windows will all be closed, but the processes will still be running. This might not be a problem. If the application simply hangs around a bit longer than normal while it tidies up, that's fine. If, on the other hand, the application hangs around for several more minutes or hours doing work after its user interface has gone away, this won't be acceptable. It might, for example, prevent the user from restarting the app later on if it holds some files open.

The best approach is usually to write your asynchronous operations so that they can be cancelled promptly, if possible, and wait for any outstanding operations to complete before letting the application shut down. This means you can carry on using asynchronous delegates while ensuring a clean and timely shutdown.

Error Handling

Errors that arise on the worker thread can typically be handled by raising an event on the UI thread so that errors are dealt with in exactly the same way as completion and progress updates. The simplest strategy is to make all errors fatal because it will be hard to arrange for error recovery on the worker thread. The best strategy for error recovery is to allow the operation to fail completely and perform the retry logic on the UI thread. If user intervention is required to fix the problem causing the error, it will be easy to bring up the appropriate prompts.

The AsyncUtils class handles errors as well as cancellation. If the operation throws an exception, the base class catches this and delivers it via a Failed event to the UI.

Conclusion

Careful use of multithreaded code can significantly improve the perceived quality of an application by preventing the UI from going to sleep when it performs a lengthy task. Asynchronous delegate invocation is the easiest way to migrate slow code out of the UI thread, avoiding such narcoleptic episodes.

The Windows Forms Control architecture is essentially single threaded, but it provides facilities to marshal calls from worker threads back onto the UI thread. The simplest strategy for dealing with notifications from the worker thread—whether they are indications of success, failure, or progress—is to treat them in much the same way as you treat events from normal controls such as mouse clicks or keyboard inputs. This avoids introducing any new idioms into the UI code, and the one-way nature of the communication makes deadlocks less likely to occur.

Sometimes it will be necessary for the UI to send a message to an operation already in progress. The most common use for this is to cancel an operation. This can be achieved by having an object that represents the call in progress and also maintains a cancellation flag which is periodically checked by the worker thread. If the user interface thread needs to wait for the cancellation to be acknowledged (either because the user needs to know that work has definitely halted or because a clean program exit is required), it gets a little more complicated, but the supplied sample code includes a base class which encapsulates all of this complexity. Deriving classes simply need to do the necessary work, periodically test for cancellation, and then inform the base class if it stopped work due to a cancellation request.

For related articles see:
Applied Microsoft .NET Framework Programming by Jeffrey Richter (Microsoft Press, 2002)
Essential .NET, Volume 1: The Common Language Runtime by Don Box (Addison Wesley Professional, 2002)

For background information see:
Safe, Simple Multithreading in Windows Forms
A Second Look at Windows Forms Multithreading

Ian Griffiths is an independent consultant in the UK specializing in .NET Windows Forms applications. He is an instructor at DevelopMentor, and is coauthor of .NET Windows Forms in a Nutshell (O'Reilly, 2002) and Mastering Visual Studio .NET (O'Reilly, February 2003).