Performing multiple Asynchronous Actions for long running operations

The Action delegate in .NET is pretty handy when you need to invoke a method that takes no arguments. You assign the method name or an anonymous delegate/lambda to an Action delegate variable and just call the delegate variable to have your method invoked.

But, what if you needed to invoke multiple actions simultaneously or wait for all actions to complete before the main thread can proceed?
Maybe these actions are responsible for pre-fetching data from a service that the main thread requires or doing some background computation whose results need to be made available before the main thread can continue. This is typical when an application is loading, wherein it needs to fetch some data or perform some initialization tasks at load time.

One of the benefits of using delegates is that they can be invoked asynchronously so that the main thread does not block. This mechanism allows you to invoke multiple actions easily but it does not provide any way to chain multiple Actions or synchronize results from calls to multiple asynchronous operations.

Another reason why it might be important to invoke background operations asyncronously is performance and responsiveness. For example, its almost always a bad idea to invoke long running operations on the UI thread since this will make the application unresponsive.
Also, with the advances in multi core technologies,the only way to make applications faster is to leverage as many cores as possible (not a subsitute for poorly written code by any means!).

Wouldn't it be nice if we could achieve these benefits without terribly complicating the client code with threading or synchronization logic.

Lets look at some code that makes this possible:

 
  1. public static class Extensions  
  2. {  
  3.     public static void ExecAsync(this IEnumerable<Action> actions)  
  4.     {  
  5.         int count = 0;  
  6.         AutoResetEvent[] events = new AutoResetEvent[actions.Count()];  
  7.         IAsyncResult[] results = new IAsyncResult[actions.Count()];  
  8.         for (int i = 0; i < events.Length; i++)  
  9.         {  
  10.             events[i] = new AutoResetEvent(false);  
  11.         }  
  12.   
  13.         foreach (var action in actions)  
  14.         {  
  15.             int localCount = count;  
  16.             results[count++]=   
  17.                 action.BeginInvoke((r) =>  
  18.                 {  
  19.                     try  
  20.                     {  
  21.                         if (r.IsCompleted)  
  22.                         {  
  23.                             Action act = r.AsyncState as Action;  
  24.                             act.EndInvoke(results[localCount]);  
  25.                         }  
  26.                     }  
  27.                     finally  
  28.                     {  
  29.                         //set the event regardless of whether there is an exception so that the main thread  
  30.                         //is not blocked indefinitely.  
  31.                         events[localCount].Set();  
  32.                     }  
  33.                 }, action);  
  34.          }  
  35.         WaitHandle.WaitAll(events);  
  36.     }  
  37. }  
  38.   
  39. class Program   
  40. {  
  41.   
  42.     public static string[] GetUsers()  
  43.     {  
  44.         //simulate a long running operation  
  45.         Thread.Sleep(3000);  
  46.         Console.WriteLine("Current Thread:{0}", Thread.CurrentThread.ManagedThreadId);  
  47.         return  new[]{"Jack","Jon","Jim"};              
  48.     }  
  49.   
  50.     public static string[] GetCountries()  
  51.     {  
  52.         //simulate a long running operation  
  53.         Thread.Sleep(4000);  
  54.         Console.WriteLine("Current Thread:{0}",Thread.CurrentThread.ManagedThreadId);  
  55.         return new[] { "US","UK","Canada" };  
  56.     }  
  57.   
  58.     public static string[] GetLanguages()  
  59.     {  
  60.         //simulate a long running operation  
  61.         Thread.Sleep(3000);  
  62.         Console.WriteLine("Current Thread:{0}", Thread.CurrentThread.ManagedThreadId);  
  63.         return new[] { "English""French""German" };  
  64.     }  
  65.   
  66.     static void Main(string[] args)  
  67.     {  
  68.         string[] countries;  
  69.         string[] users;  
  70.         string[] languages;  
  71.   
  72.         Console.WriteLine(Environment.NewLine);  
  73.         Console.WriteLine("Running Synchronously");  
  74.         Console.WriteLine("---------------------");  
  75.   
  76.         Stopwatch watch = new Stopwatch();  
  77.         watch.Start();  
  78.   
  79.         countries = GetCountries();  
  80.         users = GetUsers();  
  81.         languages = GetLanguages();  
  82.   
  83.         watch.Stop();  
  84.         Console.WriteLine("Total time taken:{0} seconds", watch.Elapsed.Seconds);  
  85.   
  86.         watch.Reset();  
  87.         watch.Start();  
  88.         Console.WriteLine("Running Asynchronously");  
  89.         Console.WriteLine("----------------------");  
  90.   
  91.         List<Action> actions = new List<Action>()  
  92.         {  
  93.             () => countries = GetCountries(),  
  94.             () => users = GetUsers(),  
  95.             () => languages = GetLanguages(),  
  96.         };  
  97.         actions.ExecAsync();  
  98.         watch.Stop();  
  99.         Console.WriteLine("Total time taken:{0} seconds",watch.Elapsed.Seconds);  
  100.     }  
  101. }  

The ExecAsync extension method accepts a collection of Actions to invoke and it takes care of invoking them asynchronously. It is also responsible for blocking the caller till all the Actions either complete successfully or throw an error. We allocate an array of AutoResetEvents corresponding to each Action.
As these Actions complete, we set the corresponding events in the finally block.
It is important to put the event setting logic in a finally block since if an error occurs, we need to have the exception propagated to the calling thread.
If we don't do this, the caller would end up waiting indefinitely!

The Program class has a few methods that provide some dummy data that the program needs. We simulate a long running operation by introducing Thread.Sleep.
In the Main method we call these operations synchronously and then asynchronously.

Output

The output clearly shows the difference in performance. The synchronous version took twice as long as the asynchronous version!
Also, the client does not contain any explicit knowledge that the code is executing asynchronously.

We can adapt the above mechanism for any kind of delegate (not just Actions) by having a corresponding extension method for the delegate type and possibly passing in the callback as a continuation.

Jeff Richter has a much more sophisticated solution with his AsyncEnumerator class which I really like.


To continue this thread, we'll look at passing in continuations for actions in a future post.

kick it on DotNetKicks.com

Print | posted @ Sunday, March 29, 2009 5:19 PM

Comments on this entry:

Gravatar # re: Performing multiple Asynchronous Actions for long running operations
by Bill at 3/30/2009 3:22 AM

Nice job with the Post. I will be going over this today at work, and develop some classes/infrastructure around the idea [I am not a fan of extension methods, for the most part - not sure in this case]. I like Jeff's AsyncEnumerator discussion as well.
Gravatar # re: Performing multiple Asynchronous Actions for long running operations
by Abhijeet at 3/31/2009 10:05 PM

Glad you liked it.
The extension method option is something I find very handy from time to time, but there is a fine line between handiness and addiction.
It's really easy to get into the habit of using(read abusing) them. If you are not careful, your types can get easily cluttered with them which can get in the way when intellisense pops up on that type.
You can certainly wrap this up into a class of its own instead.
Gravatar # re: Performing multiple Asynchronous Actions for long running operations
by Musa Kubheka at 3/12/2010 6:08 AM

Hi Abhijeet,
Thanks for this excellent post. I have used your code in my implementation of a similar thing and it works. I have a problem on mine: after calling action.ExecAsync(), I call a method to send an eMail - SendEmail() and it seems like the method is called before the action.ExecAsync finishes.

How can I find out if the all methods have finished executing before calling SendEmail()?

Please assist if you have some time.

Regards
Musa
Gravatar # re: Performing multiple Asynchronous Actions for long running operations
by Musa Kubheka at 3/12/2010 6:17 AM

Sorry Abhijeet,

Please ignore my previous post... It might be an problem on my SendEmail() method that made me think it is being called before the asynchronous method call finished.

Regards
Musa
Gravatar # re: Performing multiple Asynchronous Actions for long running operations
by Abhijeet P at 3/12/2010 11:54 PM

Hi Musa,
Glad you figured it out.
Let me know if I can be of any assistance

Regards,
Abhijeet
Gravatar # re: Performing multiple Asynchronous Actions for long running operations
by gas boilers at 5/9/2010 2:08 PM

Thanks for posting. I will try to do this at work today. Hopefully it works for me too!
Post A Comment
Title:
Name:
Email:
Comment:
Verification: