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?
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.
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.
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));
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.
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?
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.
Ha Ha I wonder if our conversation at the pub had anything to do with this ;-)
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.
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
@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
How would you pass a parameter to DoSomeWork? I don't see how, appears task must be stateless?
wekempf