Performing multiple Asynchronous Actions using TPL

I finally had a chance to install VS2010 Beta 1.Full instruction on how to here
VS2010 has undergone a major overhaul since VS 2008, a peek are some of the cool new eye candy can be found here
I've been waiting to get my hands on the TPL/PLINQ stuff and the Beta was definitely worth the wait. In a previous post we talked about using asynchronous actions for performing long running operations.

I was anxious to see how the new TPL stuff would help in achieving the same thing.

Interestingly enough, the TPL API is based on Actions as well, allowing both the generic and non-generic versions to be used for creating a Task(also supports Funcs).

The Thread pool has undergone some major rework, especially the scheduling algorithm is much more smarter now than the current implementation in.NET 3.5

The unit of abstraction is a Task as opposed to a Thread, so let's dive in and see how this looks and feels as opposed to the abstraction we previously built for asynchronous Actions. In the simplest form, the equivalent declaration for creating a Task that can execute an Action asynchronously looks like so:

 

 
  1.  public static void SomeMethod()  
  2.  {  
  3.   
  4.      // do something here....  
  5.      Console.WriteLine("In some method");  
  6.   
  7.  }   
  8.   
  9. static void Main(string[] args)  
  10. {  
  11.     var task = Task.Factory.StartNew(SomeMethod);  
  12.    task.Wait();  
  13.  }  

You start a task and Wait for it to complete. Lets create a bunch of these tasks to execute on the GetUsers(), GetCountries() and GetLanguages() method from the previous post.

 

 
  1. Task[] tasks = new Task[]  
  2. {  
  3.    Task.Factory.StartNew(() => countries = GetCountries()),  
  4.    Task.Factory.StartNew(() => countries = GetUsers()),  
  5.    Task.Factory.StartNew(() => countries = GetLanguages())  
  6. };  
  7. Task.WaitAll(tasks);  

This looks fairly similar from a usage persepctive to what we had for executing asynchronous actions

 
  1. List<Action> actions = new List<Action>()  
  2. {  
  3.     () => countries = GetCountries(),  
  4.     () => users = GetUsers(),  
  5.     () => languages = GetLanguages(),  
  6. };          
  7. actions.ExecAsync();  

One obviously noticeable difference in using the Task is that you use the Factory method StartNew to create and execute the task at the point of declaration, and what you get back is a handle to the executing Task. So the Task[] contains a handle to all the executing tasks.
The ExecAsync extension method on the other hand is passed a collection of Actions to execute. I find the idea of being able to create and execute the Task in one statement very useful and terse.

Let's now compare the performance of executing Actions using Tasks v/s executing Actions using The ExecAsync()

 

 
  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.    public class Program  
  40.    {  
  41.        public static string[] GetUsers()  
  42.        {  
  43.            //simulate a long running operation  
  44.            Thread.SpinWait(3000);  
  45.            Console.WriteLine("Current Thread:{0}", Thread.CurrentThread.ManagedThreadId);  
  46.            return new[] { "Jack""Jon""Jim" };  
  47.        }  
  48.   
  49.        public static string[] GetCountries()  
  50.        {  
  51.            //simulate a long running operation  
  52.            Thread.SpinWait(3000);  
  53.            Console.WriteLine("Current Thread:{0}", Thread.CurrentThread.ManagedThreadId);  
  54.            return new[] { "US""UK""Canada" };  
  55.        }  
  56.   
  57.        public static string[] GetLanguages()  
  58.        {  
  59.            //simulate a long running operation  
  60.            Thread.SpinWait(3000);  
  61.            Console.WriteLine("Current Thread:{0}", Thread.CurrentThread.ManagedThreadId);  
  62.            return new[] { "English""French""German" };  
  63.        }  
  64.        static void Main(string[] args)  
  65.        {  
  66.   
  67.            string[] countries;  
  68.            string[] users;  
  69.            string[] languages;  
  70.            List<Action> actions = new List<Action>()  
  71.            {  
  72.                () => countries = GetCountries(),  
  73.                () => users = GetUsers(),  
  74.                () => languages = GetLanguages(),  
  75.            };  
  76.            Console.WriteLine("Running Actions Asynchronously");  
  77.            Console.WriteLine("----------------------");  
  78.            Stopwatch watch = Stopwatch.StartNew();  
  79.            actions.ExecAsync();  
  80.            watch.Stop();  
  81.            Console.WriteLine("Total time taken:{0} milliseconds", watch.ElapsedMilliseconds);  
  82.   
  83.            Console.WriteLine();  
  84.            Console.WriteLine("Running Actions using Tasks");  
  85.            Console.WriteLine("----------------------");  
  86.            watch.Reset();  
  87.            watch.Start();  
  88.            Task[] tasks = new Task[]  
  89.            {  
  90.               Task.Factory.StartNew(() => countries = GetCountries()),  
  91.               Task.Factory.StartNew(() => countries = GetUsers()),  
  92.               Task.Factory.StartNew(() => countries = GetLanguages())  
  93.            };  
  94.            Task.WaitAll(tasks);  
  95.            watch.Stop();  
  96.            Console.WriteLine("Total time taken:{0} milliseconds", watch.ElapsedMilliseconds);  
  97.        }  
  98.    }  

Let's give it a spin and see the numbers:

The version using Tasks is almost 5 times faster! which suggests that TPL is doing a far superior job under the covers in effectively scheduling and executing the Actions.
TPL has a host of goodness which I've barely started exploring.Among other goodies, is the ability to have parent-child relationships between tasks and support for cancellation using a CancellationToken.
I hope to do more frequent posts especially around TPL since this has piqued my interest.
As always happy coding!

kick it on DotNetKicks.com

Print | posted @ Wednesday, June 17, 2009 12:50 AM

Comments on this entry:

Gravatar # re: Performing multiple Asynchronous Actions using TPL
by krlm at 11/14/2009 1:00 AM

picture of the console does not match to the code.
Gravatar # re: Performing multiple Asynchronous Actions using TPL
by Abhijeet at 11/14/2009 4:25 PM

@krlm: Thanks for pointing that out!
Seems like I accidentally linked to the console image from an older post -
"Performing multiple Asynchronous Actions for long running operations" while migrating to Skydrive.

I've fixed it now. Thanks again.
Post A Comment
Title:
Name:
Email:
Comment:
Verification: