This blog has moved, permanently, to http://software.safish.com.

Wednesday, October 13, 2010

Playing with the TPL

I did a quick investigation on the Task Parallel Library (TPL) this morning (part of the System.Threading library which comes included in the .NET 4.0 framework or is available as a download from 3.5), and I’m really amazed at how easy it was to implement, and the apparent benefits of using it.  Basically, this library allows you to benefit from the multiple cores found in modern computers, without the pain of actually coding for multiple cores yourself. 

I wrote a very simple C# application that fired off 1,000 web requests to a web application on my local machine and read the response stream.  This was done in three ways:

  1. sequentially making the web request in a for loop
  2. using a thread pool to make the web requests
  3. using Parallel.Invoke to make the web requests with the TPL library

Well, option 1 was obviously very simple to implement, but it was SLOW.  1,000 requests took in the region of 20 seconds to run.

Option 2 was the most difficult to implement, but obviously far more efficient.  I always find coding for multiple threads a little tricky, as I don’t do it often and I never remember offhand where to put the wait handlers.  Anyway, it resulted in 1,000 requests being done in approximately 11 seconds.

Option 3 was, to my pleasant surprise, a piece of cake to implement.  Create an Action list, and call Parallel.Invoke – that was it.  Incredibly, this took approximately 6.5 seconds to run on my 4-core machine.  I’ve pasted the code below, and hopefully my implementation is correct, but it’s so easy to use I don’t see why you wouldn’t use it when you have a large number of tasks to do at one time.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Net;
using System.IO;
using System.Threading;

namespace TplSample
{
    class Program
    {
        static string url = "http://dev.mysite.co.za/";
        static int testCount = 1000;

        static void Main(string[] args)
        {


            // fire off one request so we aren't skewed by a sleeping application pool
            SendWebRequest();

            // fires off requests in sequential fashion
            SingleRequests();
            // fires off requests using a thread pool
            MultiThreaded();
            // fires off requests using parallel processing
            ParallelRequests();

            Console.WriteLine("Done.");
            Console.ReadLine();
        }

        static void SingleRequests()
        {
            // fire off the web request 1,000 times, and time it
            Stopwatch sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < testCount; i++)
            {
                SendWebRequest();
            }
            sw.Stop();
            Console.WriteLine("Single requests: {0} seconds", sw.Elapsed.TotalSeconds);

        }

        static void ParallelRequests()
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            List actions = new List();
            for (int i = 0; i < testCount; i++)
            {
                Action a = new Action(SendWebRequest);
                actions.Add(a);
            }
            Parallel.Invoke(actions.ToArray());
            sw.Stop();
            Console.WriteLine("Parallel requests: {0} seconds", sw.Elapsed.TotalSeconds);
        }

        static void MultiThreaded()
        {
            // ThreadPool.SetMaxThreads(10, 10);
            Stopwatch sw = new Stopwatch();
            sw.Start();

            List doneEvents = new List();
            for (int i = 0; i < testCount; i++)
            {
                ManualResetEvent resetEvent = new ManualResetEvent(false);
                doneEvents.Add(resetEvent);
                ThreadWrapper tw = new ThreadWrapper(resetEvent);
                WaitCallback callBack = new WaitCallback(tw.ThreadPoolCallback);
                ThreadPool.QueueUserWorkItem(callBack);

                if (doneEvents.Count == 64)
                {
                    WaitHandle.WaitAll(doneEvents.ToArray());
                    doneEvents.Clear();
                }
            }

            if (doneEvents.Count > 0)
            {
                WaitHandle.WaitAll(doneEvents.ToArray());
            }
            sw.Stop();
            Console.WriteLine("Thread pool requests: {0} seconds", sw.Elapsed.TotalSeconds);
        }

        static void SendWebRequest()
        {
            HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
            req.Method = "GET";

            HttpWebResponse webResponse = (HttpWebResponse)req.GetResponse();
            using (StreamReader responseStream = new StreamReader(webResponse.GetResponseStream()))
            {
                string response = responseStream.ReadToEnd();
                responseStream.Close();
            }
            webResponse.Close();
            //Console.WriteLine("1");
        }

        public class ThreadWrapper
        {

            private ManualResetEvent doneEvent;


            public ThreadWrapper(ManualResetEvent doneEvent)
            {
                this.doneEvent = doneEvent;
            }

            public void ThreadPoolCallback(Object threadContext)
            {
                SendWebRequest();
                this.doneEvent.Set();
            }


        }
    }
}