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:
- public static class Extensions
- {
- public static void ExecAsync(this IEnumerable<
Action> actions)
- {
- int count = 0;
- AutoResetEvent[] events = new AutoResetEvent[actions.Count()];
- IAsyncResult[] results = new IAsyncResult[actions.Count()];
- for (int i = 0; i < events.Length; i++)
- {
- events[i] = new AutoResetEvent(false);
- }
-
- foreach (var action in actions)
- {
- int localCount = count;
- results[count++]=
- action.BeginInvoke((r) =>
- {
- try
- {
- if (r.IsCompleted)
- {
-
Action act = r.AsyncState as Action;
- act.EndInvoke(results[localCount]);
- }
- }
- finally
- {
-
-
- events[localCount].Set();
- }
- }, action);
- }
- WaitHandle.WaitAll(events);
- }
- }
-
- class Program
- {
-
- public static string[] GetUsers()
- {
-
- Thread.Sleep(3000);
- Console.WriteLine("Current Thread:{0}", Thread.CurrentThread.ManagedThreadId);
- return new[]{"Jack","Jon","Jim"};
- }
-
- public static string[] GetCountries()
- {
-
- Thread.Sleep(4000);
- Console.WriteLine("Current Thread:{0}",Thread.CurrentThread.ManagedThreadId);
- return new[] { "US","UK","Canada" };
- }
-
- public static string[] GetLanguages()
- {
-
- Thread.Sleep(3000);
- Console.WriteLine("Current Thread:{0}", Thread.CurrentThread.ManagedThreadId);
- return new[] { "English", "French", "German" };
- }
-
- static void Main(string[] args)
- {
- string[] countries;
- string[] users;
- string[] languages;
-
- Console.WriteLine(Environment.NewLine);
- Console.WriteLine("Running Synchronously");
- Console.WriteLine("---------------------");
-
- Stopwatch watch = new Stopwatch();
- watch.Start();
-
- countries = GetCountries();
- users = GetUsers();
- languages = GetLanguages();
-
- watch.Stop();
- Console.WriteLine("Total time taken:{0} seconds", watch.Elapsed.Seconds);
-
- watch.Reset();
- watch.Start();
- Console.WriteLine("Running Asynchronously");
- Console.WriteLine("----------------------");
-
- List<
Action> actions = new List<Action>()
- {
- () => countries = GetCountries(),
- () => users = GetUsers(),
- () => languages = GetLanguages(),
- };
- actions.ExecAsync();
- watch.Stop();
- Console.WriteLine("Total time taken:{0} seconds",watch.Elapsed.Seconds);
- }
- }
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.
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.