Welcome to WindowsClient.net | Sign in | Join


“EasyThread” – Quick and Easy Application Threading

Roland Fernandez - Microsoft Strategic Prototyping Team, May 4, 2004

Download the Installer and Sample App
Download the Source Code

 

Introduction

Many Windows applications start out in life as a single-threaded application where the work they do in response to UI events is relatively short-running.  At this stage, the foreground thread is readily available to service new UI events, the application window appears responsive to the user, and life is good. 

 

When long-running work (accessing LAN files, calling a Web Service, executing SQL queries, scanning directories, doing loop-intensive calculations, etc) is added in the same style (a direct call from the UI event handling code), the window will sometimes appear “frozen” – it can be difficult to move/resize, the controls will not respond to mouse movements, clicks, or keyboard keystrokes, and the window will sometimes only be partially painted.  This happens because the new UI events being generated (the Windows messages sent to the registered Window procedure) are not being serviced by the foreground thread; the current event handler code has not yet been returned from the call to the long-running work. 

 

Solutions

A first order solution that can be quickly applied is to have the application inform the user that a long running task in executing and the application will temporarily be unavailable for any new interaction or requests.  This can go a long way in avoiding user confusion and their feeling of not being in control.  In some cases, this solution alone may suffice, but in many cases, users will demand to continue working in the app in parallel with the long running work being done.

 

The real solution is to move the long-running work onto a background thread, but here is where things traditionally get hard.  A developer has to create a new thread, figure out to pass the parameters or “state” to it, determine where data locking is needed, avoid deadlocks, and make sure that any code that accesses a Windows control is run on the foreground thread (or whichever thread created the control).  In writing code to support all of these requirements, a developer’s “flow” will definitely be interrupted (sometimes for several days), and the resulting source code will be larger, harder to maintain, and his original application algorithm will sometimes be mangled beyond recognition.

 

EasyThread is a .NET add-in (currently for C# and VS.NET 2003) and a set of threading guidelines that makes this process much easier.

 

Using EasyThread

EasyThread comes with a setup.exe that automatically registers it with VS.NET 2003.  Once EasyThread has been installed, you can open an application and start using it.

 

The guidelines for using EasyThread to keep Windows client apps responsive (and users feeling in control and productive) are:

 

1.      Move any event handlers that are long running onto their own background thread.  This can be done by adding the “[BgThread]” attribute in front of the event handler method.  I recommend creating a separate, well-named method for this step that is called from the VS.NET-generated event handler methods.  There is no need to use delegates or funny parameters here; just treat it like a normal method call.  The main restriction here is that it must be a “void” return type (since it is called asynchronously).  See the “Known Issues” section below for a more complete list of restrictions.

 

How long is “long running”?  It depends on several factors, such as how often the events occur, the time to handle the events, the patience of the end-user, etc.  I recommend trying to keep the window and controls responding in the way that seems to be “instant” (under .25 secs) so that the end-user feels in control of the application.

 

2.      Within this code now running on a background thread, run any code blocks that need to access a Windows control or any object data on the foreground thread.   This can be done by moving them into their own method and adding the “[FgThread]” attribute in front of those methods.  This will suspend the background thread execution while the code runs on the foreground thread. 

 

This solves the problem of having to move  to the foreground thread to access Windows controls and it has the nice by-product of minimizing the need for locking (since the foreground thread will only run one of these “atomic blocks” of code at a time and not overlap them with each other or short-running work).  The call to the foreground methods need no delegates and can use normal parameters and return types. 

 

Note that this process of identifying blocks of code in background threads that access shared data (object data, class data, and Windows controls) and moving them to the foreground thread is a lot like an obtaining an application level lock – it “serializes” the blocks and ensures that only one block will execute at a time (from start to finish). This makes the data safe to access within the block. 

 

How EasyThread Works

EasyThread was created using the VS.NET Add-In Wizard, which generates an empty add-in app.  Once built, it is registered with VS.NET and ready to drive the VS.NET object model. 

 

EasyThread is currently fairly small (about 600 lines of C# code).  It uses the VS.NET “dte” interface to enumerate all projects in the current solution, all classes in each project, and all methods with each class.  It looks for the “FgThread” and “BgThread” attributes on these methods and generates two helper methods for each of them (the first one accepts normal params and calls the second one using the required managed parameter forms.  The second method runs on the desired fg/bg thread  and calls the user’s target method, whose name is modified by adding an underscore (“_”) to the front of it.   All of the call redirection is done by the helper, but the developer should be aware that the target method will be slightly renamed.

 

All of the helpers within a class are collected together in a #region so that they can easily be collapsed using the VS.NET outlining feature.  The helpers are only rewritten when needed (when parameters change, for example).  The actual code generation is done by inserting text using an “EditPoint” object.

 

At any point, you can just delete the entire generated helper #region and it will be rebuilt the next time you start to compile.

 

The EasyThread Demo

EasyThread ships with a demo/test program called “Test2”.  It contains a single form (“Form1”) that, when run, demonstrates a responsive UI (the “Count” button) and a long-running event handler that freezes the UI (the “Build List” button).  I recommend building the project and running to see how thing work before and after the “Build List” button is pressed.

 

To fix this app, you should apply the guidelines above.  If you run into a problem, you can peek at the Form1After.cs file in the same directory (not included in the project build).  Once you get the app fixed using EasyThread, for extra credit, you can try to figure out how you would stop the background building of the list when the “Clear List” button is pressed.

 

Current Known Issues

    - The EasyThread Add-in sometimes gets unloaded by VS.NET (or not initially loaded).  The solution is:

a.       Open the Add-In Manager dialog (Tools | Add In Manager…)

b.      Uncheck the EasyThread entry (in the “Available Add-Ins” column)

c.       Close the dialog with OK

d.      Repeat steps a-c (but this time check the EasyThread checkbox).

 

-           “ref” and “out” parameters are not supported for fg/bg methods

 

-          EasyThread ignores the “public” or “protected” keywords on fg/bg methods; all generated helpers are private.

 

-          Method overloading is not currently supported for fg/bg methods

 

-          When parameters within a fg/bg method are changed, the helper methods are not updated correctly.  This results in a compiler error – the solution is to remove the EasyThread generated helpers in question (or the entire EasyThread-generated region) and recompile.  The helpers will be correctly regenerated.

 

- After a method is deleted, the helper code remains.  It can be left in the program or manually deleted.

 

-          When an exception gets thrown in a FgThread/BgThread call, it gets reported on the helper.  Put a try/catch block in your target method to find where the exception is really happening

 

-          Marking a VS.NET generated event handler with the [BgThread] or [FgThread] attribute may confuse Visual Studio.  I suggest using a separate method called from the event handler.