Tale of Two Worker Items (.NET/C#)

Before .NET 4 the recommended way to create a new worker item thread was using the thread pool to queue up a work item.

ThreadPool.QueueUserWorkItem(new WaitCallback(DoWork));

private void DoWork(object state)
{
Console.WriteLine("Thread {0} doing work",
Thread.CurrentThread.ManagedThreadId);
Thread.Sleep((new Random()).Next(5, 5000));
Console.WriteLine("Thread {0} DONE work",
Thread.CurrentThread.ManagedThreadId);
}

There is nothing wrong with continuing to use the ThreadPool pattern to queue up worker thread. In fact using this pattern gets a performance boost you can see with just the enhancements Microsoft added to the ThreadPool in .NET 4. However .NET 4 also contains a whole set of features to facilitate parallel programming via the Tasks namespace. I would not recommend switching existing code to use the new features for the sake of using new feature. For the new code however it is worth taking a look to see if any of the new features would be worth using. Using tasks we could queue up worker threads in a similar matter.

new Task(DoWork, null).Start();

private void DoWork(object state)
{
Console.WriteLine("Thread {0} doing work",
Thread.CurrentThread.ManagedThreadId);
Thread.Sleep((new Random()).Next(5, 5000));
Console.WriteLine("Thread {0} DONE work",
Thread.CurrentThread.ManagedThreadId);
}

One line code change, functionally it seems to work the same, and it is using a newer paradigm. Done right? Just because it’s newer doesn’t mean it’s better. Let’s take a look ‘under the hood’ to really see what they differences are between the two code block above.

When talking about ‘under the hood’ with .NET that basically is a code phrase for ‘opening up Reflector’. Taking a look at what actually happens when we call ThreadPool.QueueWorkerItem we can boil it down to a fairly straight forward sequence of events.

  1. Ensures that the ThreadPool VM is initialized (calls into the CLR for this via a QCall)
  2. Wraps the WaitCallback inside an ExecutionContext call back. This preserves the calling state among some other minor details
  3. Queues the item onto the Global ThreadPool

The ‘meat and potatoes’ of what is being done can be done by just viewing the ThreadPool.QueueUserWorkItemHelper method.

Task.Start.Enqueue.png

At some point then the work is Dequeued by a thread. Dequeuing in .NET 4 actually has some complexities behind it with WorkStealingQueues and other magic. For now we can be contented with the fact that all ThreadPool.QueueUserWorkItem does is put a callback on a Queue for the framework to pick up at a later date.

Now what about using Tasks and Task.Start? The sequence of events is slightly more complex.

  1. Calls Task.Start with the default TaskScheduler (which is the ThreadPoolScheduler)
  2. Checks the current state of the tasks to ensure the task can be schedules
  3. QueueTask is called on the current TaskScheduler (again by default this is ThreadPoolScheduler)

That is pretty much what the Task does, now the responsibility of scheduling the task is passed to the TaskScheduler (imagine that). Taking a look the default ThreadPoolScheduler.

  1. Looks at the options passed in
    1. If TaskCreationOptions.LongRunning pass passed in the scheduler treat this a hint to create a regular .NET Thread set as a BackgroundWorker. Because user options are tied in with the state of the ThreadPoolScheduler there is no guarantee that this code path will be taken.
    2. Otherwise ThreadPool.UnsafeQueueCustomWorkItem is called to queue up the item.
      1. If the Task was started from within a Task the WorkerItem is queued up on a local ThreadPoolQueue
      2. Otherwise the Task is queued up in the Global ThreadPool – This is what happens in the code sample above

There is more logic going on up front with the Task (for a great article on what is going on I would recommend http://msdn.microsoft.com/en-us/library/dd997402.aspx). For the typical usage pattern though Task.Start will eventually call ThreadPool.UnsafeQueueCustomWorkItem.

Task.Start.png

Which then queue up the item to be dequeued by the same ThreadPool as if we used ThreadPool.QueueUserWorkItem.

So for typicalusage there will be little difference between using ThreadPool.QueueUserWorkItem and using Task.Start. However there are some subtle differences between the two. Ultimately which way you want to use is up to you. Me? I prefer using Task.Start, mostly for the flexibility it provides (plus ‘OOOH! SHINY!’).

Note: This entry also posted on tech.collectedit.com

Leave a Reply