ThreadPool vs. Tasks

.NET 4.0 includes a new few new classes called Tasks, which are part of the Task Parallel Library. You can learn all about them in an article by my friend Sacha on Code Project.

The TPL is useful, but I'm starting to see a lot of coders using the Task class. I may be an old fuddy-duddy, but I can't quite understand what advantage Task gives me over plain old ThreadPool in .NET 2.0. Here are some examples of how the Tasks "features" would be implemented using ThreadPool.

Starting a task

ThreadPool.QueueUserWorkItem(delegate
{
    DoSomeWork();
});

Waiting for a task to complete

var handle = new ManualResetEvent(false);
ThreadPool.QueueUserWorkItem(delegate
{
    DoSomeWork();
    handle.Set();
});
handle.WaitOne();

Returning

int result = 0;

ThreadPool.QueueUserWorkItem(delegate
{
    DoSomeWork();

    result = 42;

    handle.Set();
});
handle.WaitOne();

Console.WriteLine(result);

Chaining tasks

var handle = new AutoResetEvent(false);
ThreadPool.QueueUserWorkItem(delegate
{
    DoSomeWork();
    handle.Set();
});
handle.WaitOne();

ThreadPool.QueueUserWorkItem(delegate
{
    DoSomeMoreWork();
    handle.Set();
});
handle.WaitOne();

Waiting for multiple tasks to complete

var handle1 = new ManualResetEvent(false);
var handle2 = new ManualResetEvent(false);

ThreadPool.QueueUserWorkItem(delegate
{
    DoSomeWork();
    handle1.Set();
});

ThreadPool.QueueUserWorkItem(delegate
{
    DoSomeMoreWork();
    handle2.Set();
});

WaitHandle.WaitAll(new WaitHandle[] { handle1, handle2 });

Exception handling

var handle = new ManualResetEvent(false);

Exception error = null;
ThreadPool.QueueUserWorkItem(delegate
{
    try
    {
        DoSomeWork();
    }
    catch (Exception ex)
    {
        error = ex;
    }
    finally
    {
        handle.Set();
    }
});

handle.WaitOne();

if (error != null)
    Console.WriteLine("Error! " + error);

Word of caution: you should probably always do this when using ThreadPool, since exceptions thrown on a ThreadPool thread will tear down the AppDomain if not caught

Cancellation

var cancel = false;

ThreadPool.QueueUserWorkItem(delegate
{
    while (!cancel)
    {
        Thread.Sleep(100);
    }
});

cancel = true;

Note: this is like a gazillion times more complex in Tasks

Dispatching to UI thread

var dispatcher = Application.Current.Dispatcher;

ThreadPool.QueueUserWorkItem(delegate
{
    DoSomeWork();

    dispatcher.BeginInvoke(new Action(() => progressBar.Value += 10));

    DoSomeMoreWork();

    dispatcher.BeginInvoke(new Action(() => progressBar.Value += 10));
});

Being notified when a task is complete without blocking

Action<int> done = (int x) => Console.WriteLine("Done! " + x);

ThreadPool.QueueUserWorkItem(delegate
{
    DoSomeWork();

    done(42);
});

So help me learn the Task API - how would using Task make the examples above look better?

A picture of me

Welcome, my name is Paul Stovell. I live in Brisbane and work on Octopus Deploy, an automated deployment tool for .NET applications.

Prior to founding Octopus Deploy, I worked for an investment bank in London building WPF applications, and before that I worked for Readify, an Australian .NET consulting firm. I also worked on a number of open source projects and was an active user group presenter. I was a Microsoft MVP for WPF from 2006 to 2013.

19 Aug 2011

Wow, Paul, first post where I don't think you get it at all. :)

First, several of your examples are wrong. For instance, your example on "Chaining Tasks" didn't chain anything. You made one of the worst sins you can make with that example: you turned asynchronous code into synchronous code. With Task.ContinueWith everything remains asynchronous.

Then there's the fact that nearly every single one of your examples is a lot more complex than the equivalent example done using Task. More complex means more chance of error, and errors in asynchronous code are very difficult to deal with.

Also, there's performance concerns. A lot of the patterns you're using are simply not as performant as the equivalent code using Task.

Now let's also think about the future. The Async CTP is amazing, and it simply couldn't be done without Task.

Task simplifies a lot of code, adds uniformity (cancellation uniformity means that library code you call can participant in cancellation, which won't work with your example), enables composition and allows extensions along the lines of Async CTP.

Oh, and I choked on the "this is like a gazillion times more complex in Tasks" comment. Really? I think it's simpler with Task, and no one can seriously argue it's a "gazillion times" more complex!

I post something to my blog later illustrating how every one of your examples is simpler with Task.

19 Aug 2011

I've grown rather fond of Rx. It works particularly well for scenarios where there's no need to block while waiting for results. It has a nice IScheduler interface and a set of standard schedulers, including a ThreadPoolScheduler (and a TaskPoolScheduler).

Some of your examples reworked to use Rx/ThreadPoolScheduler:

Starting a task

Scheduler.ThreadPool.Schedule(DoSomeWork);

Waiting for a task to complete

Observable.ToAsync(DoSomeWork)().First();

Returning

var result = Observable
    .ToAsync(
        () =>
        {
            DoSomeWork();
            return 42;
        })()
    .First();

Console.WriteLine(result);

Chaining tasks

Observable
    .ToAsync(DoSomeWork)()
    .Do(_ => DoSomeMoreWork())
    .First(); // omit to avoid blocking

Waiting for multiple tasks to complete

Observable
    .ToAsync(DoSomeWork)()
    .Concat(Observable.ToAsync(DoSomeMoreWork)())
    .Last();  // omit to avoid blocking

Exception handling

Observable
    .ToAsync(DoSomeWork)()
    .Subscribe(
        _ => Console.WriteLine("Success!"),
        e => Console.WriteLine("Error! " + e));

Being notified when a task is complete without blocking

Observable
    .ToAsync(DoSomeWork)()
    .Subscribe(result => Console.WriteLine("Done! " + result));
Cameron MacFarland
Cameron MacFarland
20 Aug 2011

Tasks is like LINQ.

Sure you can do the equivalent thing with a for loop and an if statement, but LINQ gives you a simpler syntax.

Simpler syntax means fewer bugs and easier to understand code.

20 Aug 2011

I wrote my blog post and you can find it here: http://digitaltapestry.net/blog/threadpool-vs.-tasks.

Paul, does this help you to understand why you should get on the Task bandwagon?

22 Aug 2011

Paul, your Cancellation example worries me. On face value, this operation isn't suitable for ThreadPool or TaskPool!

Could you provide some more information?

You're blocking a ThreadPool thread. This will have a negative impact on any other operations enqueued on the ThreadPool. I suspect that your real use case wouldn't involve a Thread.Sleep(100) but I can only guess.

Tasks are designed for asynchronous / cooperative threading, while(!cancelled) sounds more like a dedicated thread to me.

22 Aug 2011

Ha Ha I wonder if our conversation at the pub had anything to do with this ;-)

Quinton
Quinton
24 Aug 2011

Perhaps Paul has many fond memories of the old Threadpool and like myself doesn't want to let go of it yet as a few old projects are still using it. There is also the slight lurning curve moving over to Tasks and Linq, well personally for myself there was, hehe.

Kevin Jones
Kevin Jones
24 Aug 2011

Hmmm. You're using Events which are really expensive, they're kernel objects and should be avoided unless necessary such as in cross-process synchronization.

Cancellation in the Task APIs will cancel tasks before they've been scheduled, unlike your code where the task will only be cancelled after it's consumed a thread

26 Aug 2011

@Mike Strobel

Your example on chaining won't work if you remove the First() call.

Observable
    .ToAsync(DoSomeWork)()
    .Do(_ => DoSomeMoreWork())
    .First(); // omit to avoid blocking

You need to make it this instead.

Observable
    .ToAsync(DoSomeWork)()
    .Do(_ => DoSomeMoreWork())
    .Select(_ => {}); // omit to avoid blocking
Q
Q
26 Aug 2011

How would you pass a parameter to DoSomeWork? I don't see how, appears task must be stateless?