Click here to Skip to main content
15,867,568 members
Articles / Programming Languages / C#
Article

Towards Cleaner Code, A C# Asynchronous Helper

Rate me:
Please Sign up or sign in to vote.
4.92/5 (59 votes)
1 Oct 2008CPOL6 min read 72.7K   402   125   22
An asynchronous helper class in C# that adds flexibility and greatly reduces the code required to invoke and track an async task

Introduction

In any reasonably large application, there are a lot of times when you need to perform a quick task asynchronously. One use off-hand is an application that saves a log or status report to file on start-up. The operation is slow in relative terms since it involves file I/O, and we don't have to worry much about other code paths depending on it. Making it asynchronous will speed up our start time with no risk of side effects.

The .NET Framework makes asynchronous processing quite easy, we'll show a few common ways to accomplish it.  However there are 'gotchas', and keeping track of everything can lead to subtle mistakes and messier code, so I'll introduce a helper class to make our lives easier. 

Background

I'm a sucker for clean and beautifully groomed code. In my experience, maintaining a large code base with few resources is in fact the only way to keep sanity and keep from sliding into a pit of spaghetti. I've been on a push to find areas of necessarily messy or repeated code and find ways to make them more elegant and maintainable - this is the first in a series of small classes and helpers that I kick myself for not doing sooner.

The Obvious and Ubiquitous Solutions

Let's say we have a method as such that we need to run asynchronously:

C#
// The method we need to invoke //
private void SaveReport() {
	statusReport.Save(); //This call takes a bit of time
	Console.WriteLine("Status report saved!");
} 

The first thing many people think of is to create a thread as shown below. Besides the chattiness here, this is not the most efficient way to get this done, unless you have a pretty long running task. For every action, a new thread with context and stack space will be created, just to be used for a short period.

C#
// Starting an async task with a thread //
ThreadStart ts = new ThreadStart(SaveReport);
Thread t = new Thread(ts);
t.IsBackground = true; //important if we don't want this to prevent the app from exiting!
t.Name = "MyThread"; //so we can track it during debugging
t.Start();

It is more effective to utilize the .NET ThreadPool, and let it reuse a few pre allocated threads on your behalf. One way to use the ThreadPool in a round-about way is to make a delegate and begin invocation on it directly.

C#
// Invoking a delegate into the threadpool //
MethodInvoker dlg = new MethodInvoker(SaveReport);
dlg.BeginInvoke(null, null); 

There is a problem with both of those examples as well. The BeginInvoke method takes two parameters, an AsyncCallback for notifying when the operation completes, and an object that is passed through to that callback for the callers purposes, typically the original delegate. Now creating a callback just adds more trouble to this whole exercise, particularly if you do not need to know exactly when the operation completes. However, although not documented well, I've seen issues if the callback is not used to call EndInvoke.

The last way I'll show is to go straight after the ThreadPool.

C#
// Queuing directly into the threadpool //
WaitCallback wc = new WaitCallback(SaveReport);
ThreadPool.QueueUserWorkItem(wc);

This is much more concise, but without the control over the process that we have in the above examples. A larger problem that I haven't discussed is that any of these examples can fail spectacularly, or sometimes silently - any exceptions from your invoked methods will vanish into the ether. Once error handling (and logging, right?) is applied to both the action being performed, and the action of invoking it, and possibly to ending the invoke, we have a lot of code to just make a one-liner action happen safely asynchronous.

If this does fail, you may want to then attempt the action with a dedicated thread to make sure it works if the ThreadPool is not cooperating, that's more code to bandy about.

A more subtle limitation I started to run into is the matter of reentrance. In the above example, we are periodically saving a status report. What happens if it gets delayed? It's quite possible that the next time you invoke it the prior attempt is still in process, all kinds of mayhem could ensue. Now we would need to add even more code for blocking and thread safety for tasks that might get invoked more than once.

The Solution - Using the Code

What if we had some kind of magical helper class that had the power of any of the examples above, but was also incredibly concise? It would put all of this power in one place, so troubleshooting and changes and error handling and tweaks could be concentrated in one place. Come to think of it, such a class might even heal sick puppies! Here's how we would use it.

C#
// Using our Async helper //
Async.Do(SaveReport);

What? How could it be that easy? Is this possible? Yes, all this can be yours, and more. .NET automatically takes any void method with no parameters and invisibly makes a delegate for it, which is how the above works by passing a method name, we can also pass delegates or anonymous methods.

C#
// Using our Async helper with an anonymous delegate//
Async.Do(delegate {
    Console.WriteLine("I'm in an anonymous delegate!");
    statusReport.Save();
});

That saved us from even having to define the first method - just add some code and go.

More Advanced Usage

Now we can get into more functionality, that would take some time to implement for each task normally. With some overloading and fancy footwork, we can also do any of the following..

C#
/* this takes care of any of our locking and blocking and thread-safety woes. 
The ReentranceMode enumeration also has options for 'Stack' and 'Allow' */

Async.Do(SaveReport, ReentranceMode.Bypass); 

We can also track this task and know when it completes, or use the AsyncResult to abort it. The Do method returns a custom class that implements the IAsyncResult interface, with much more functionality than the typical .NET Framework usages.

C#
/* The bool parameter 'useReturnValue' instructs the helper to track 
the return value of your delegate/method if you ask for it later from 
the IAsyncResult that is returned : */

IAsyncResult result = Async.Do(SaveReport, true);
//do other stuff in meantime…
// …and if the action is not completed, wait for it to finish
if (!result.IsCompleted) result.AsyncWaitHandle.WaitOne();
Console.WriteLine("My result was: " + result.ReturnValue);

Instead of defining a method, save time for small tasks by using an anonymous method.

C#
/* Methods that take parameters can be wrapped in an anonymous method, 
the parameter will be psuedo-curryed for us by the framework. 
If the return value is important, 
we can also use a return inside the anonymous method! */

Async.Do(delegate { return GetAnObject("A string parameter"); }); 

And finally, the most advanced overload of the 'Do' method..

C#
/* our most advanced method signature has all of the options, as seen below */
Async.Do(
   SaveReport, //our delegate or method
   false, //track return value?
   this, // a state object to be tracked with the IAsyncResult
   true, /* Use ThreadPool? true will attempt to use the threadpool, 
            then fall back to a thread.
            False always uses a thread, for use with long-running tasks.*/
   ReenteranceMode.Stack); //our enum controlling how we handle multiple 
                           //calls to the same action. 

One last example. What if you need to run multiple tasks and know when they are all complete? A good example of this I found was for closing a collection of network listeners, each one could block for a while so we should close them in parallel but wait for them all to complete before exiting the application.

C#
List<waithandle /> wait = new List<waithandle />();

foreach (Listener l in listeners) {
   IAsyncResult result = Async.Do(l.CloseNetwork);
   wait.Add(result.AsyncWaitHandle);
}
//...finish cleaning up a few more things in the meantime....

//and then wait for all of the waithandles to complete.
WaitHandle.WaitAll(wait.ToArray());

Points of Interest

First you might wonder how this class works. I didn't want to get too long in this article, but my blog has a follow-up with all the details of how it was implemented, check it out.

I think ReentranceMode needs a little more attention, as it is a very important feature of this class. The ReentranceMode enum has three options; Allow, Bypass, and Stack.

Allow: In this case, there are no locking checks. If you run a task, and then immediately run it again (possibly from a totally different area of your app) Allow will let them both go at the same time. Find if you aren't accessing any shared resources, say for logging or the like.

Stack: With stackmode, the class will detect if you already have a task executing in the same method (including anon. methods/delegates). It will block before the task begins, and continue once the prior copy completes. Good for shared resources like writing to a file, but be careful — if you get too many stacked up it could balloon your memory usage.

Bypass: The most common that I use, bypass will detect prior instances like stack does, but if it finds one already executing it dumps out immediately without running. This is good for things like internal checks or other tasks that need to run occasionally, but not necessarily back-to-back.

In Part II we will add on to this Async class giving it the functionality to invoke tasks on to the GUI thread as well, making safe multithreading easy in Windows Forms apps.

This solution provides all the power of any other methodology with none of the bloat and maintainability issues. This class has greatly simplified code flow and literally cut thousands of lines of code from a large application I work with, so I hope it can be of use to you as well!

History

  • 25th September, 2008: Initial version
  • 1st October, 2008: Added link to second version and expanded Points of Interest

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect CodeToast.com
United States United States
Nicholas is the Chief Developer at SAM Systems, a software company that specializes in video surveillance, compression, and streaming. We have a full surveillance and compression platform for Windows, with SDK: www.samipvideo.com.

Nick is especially interested in high-performance, multi-threaded/parallel code, video processing, cross-platform development, and GUI design. He maintains a blog about software development and business at www.codetoast.com - check there for more detail on articles posted here, and other musings about the technology world.

I also play guitar, collect toasters and dead hard drives, and apparently like writing overly formal bio's in the third person.

Comments and Discussions

 
GeneralVery kool Pin
Donsw22-Jan-09 7:19
Donsw22-Jan-09 7:19 
GeneralGive you five Pin
delong18-Dec-08 4:37
delong18-Dec-08 4:37 
QuestionComparison to BackgroundWorker Pin
spaceboy14-Nov-08 6:13
spaceboy14-Nov-08 6:13 
Generalsilverlight support Pin
Huisheng Chen17-Oct-08 17:25
Huisheng Chen17-Oct-08 17:25 
Generalthanks for greate article Pin
Alezy803-Oct-08 2:10
Alezy803-Oct-08 2:10 
GeneralRe: thanks for greate article Pin
Nicholas Brookins6-Oct-08 6:10
Nicholas Brookins6-Oct-08 6:10 
Generalexcellent article, tasty code Pin
BillWoodruff29-Sep-08 20:58
professionalBillWoodruff29-Sep-08 20:58 
GeneralRe: excellent article, tasty code Pin
Nicholas Brookins30-Sep-08 2:12
Nicholas Brookins30-Sep-08 2:12 
GeneralTypes Pin
Seth Morris29-Sep-08 16:55
Seth Morris29-Sep-08 16:55 
AnswerRe: Types Pin
Nicholas Brookins29-Sep-08 17:30
Nicholas Brookins29-Sep-08 17:30 
GeneralGreat idea, great explanation, great code... man great everything Pin
Necromantici29-Sep-08 16:02
Necromantici29-Sep-08 16:02 
GeneralRe: Great idea, great explanation, great code... man great everything Pin
Nicholas Brookins29-Sep-08 16:42
Nicholas Brookins29-Sep-08 16:42 
GeneralRe: Great idea, great explanation, great code... man great everything Pin
Necromantici29-Sep-08 17:26
Necromantici29-Sep-08 17:26 
QuestionNotification from the thread? Pin
Ben Robbins29-Sep-08 15:18
Ben Robbins29-Sep-08 15:18 
AnswerRe: Notification from the thread? Pin
Nicholas Brookins29-Sep-08 16:38
Nicholas Brookins29-Sep-08 16:38 
AnswerA little more information on the Reentrance option Pin
Nicholas Brookins26-Sep-08 6:38
Nicholas Brookins26-Sep-08 6:38 
GeneralGreat stuff Pin
jmw26-Sep-08 5:32
jmw26-Sep-08 5:32 
GeneralRe: Great stuff Pin
Nicholas Brookins26-Sep-08 6:41
Nicholas Brookins26-Sep-08 6:41 
GeneralGreat Idea Pin
merlin98126-Sep-08 4:01
professionalmerlin98126-Sep-08 4:01 
GeneralRe: Great Idea Pin
Nicholas Brookins26-Sep-08 4:14
Nicholas Brookins26-Sep-08 4:14 
GeneralNice Pin
Paul Brower26-Sep-08 1:22
Paul Brower26-Sep-08 1:22 
GeneralRe: Nice Pin
Nicholas Brookins26-Sep-08 4:14
Nicholas Brookins26-Sep-08 4:14 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.