“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.