Parallel Programming in .NET 4.0: Using Tasks
So far in this series, we have discussed PLINQ and the Parallel class. These allow you to take existing code (LINQ queries, for/foreach loops, etc.) and run it in parallel. In today’s post, let’s examine how you might leverage .NET 4.0 if you are currently doing lower-level parallel programming using the Thread class.
Instead of Threads, you can consider using Tasks. The Task class (in System.Threading.Tasks) allows you to execute asynchronous work. Among other things, Tasks support waiting, cancellation, continuations, robust exception handling, detailed status, and custom scheduling, which makes them the preferred choice over Threads for parallel programming.
Let’s walk through some common scenarios with Tasks.
How do I start a Task?
There are a couple of different ways to start Tasks. If you want to create and start the task at the same time, use the following code. (In this case, task 1 simply writes a line of text to the console.)
Task task1 = Task.Factory.StartNew(() => Console.WriteLine("Task 1 is doing work."));
How do I chain Tasks together?
There is great flexibility with Tasks, to be able to chain tasks together so that Task 2 will start after Task 1 completes. You can also pass results from prior tasks into subsequent tasks.
Task.Factory.StartNew(() =>
{
// Execute Task 1 work here...
return "Some Relevant Data";
}).ContinueWith(t =>
{
string result = t.Result; // can use the result of previous task
// Execute Task 2 work here...
}, TaskScheduler.FromCurrentSynchronizationContext());
How do I cancel a Task?
You also have the ability to cancel running tasks. This can be useful especially with long-running tasks or tasks performed from a UI with a “Cancel” button for the user. Imagine a scenario where you use the multiple cores on your machine to run multiple long-running tasks in parallel; you can use the results of whichever task finishes first and easily cancel the rest of them.
To create a cancelable task, use a CancellationTokenSource and CancellationToken (both in the namespace System.Threading). Pass the cancellation token into the Task, and in the Task, check if cancellation has been requested.
CancellationTokenSource tokenSource = new CancellationTokenSource();
CancellationToken token = tokenSource.Token;
var task = Task.Factory.StartNew(() =>
{
// Were we cancelled already, before we started?
token.ThrowIfCancellationRequested();
bool moreToDo = true;
while (moreToDo)
{
// Do the actual work here. Set moreToDo = false when you're done.
// Poll on this property if you have to do
// other cleanup before throwing.
if (token.IsCancellationRequested)
{
// Clean up here, then...
token.ThrowIfCancellationRequested();
}
}
}, tokenSource.Token); // Note that we're passing the same token to StartNew.
From other code, you would call tokenSource.Cancel() to cancel the task when appropriate. Then the task above would know to respond accordingly.
Finally, let’s conclude with a sample that uses Tasks from the fabulous collection at https://code.msdn.microsoft.com/ParExtSamples. (Remember that Stephen Toub gives a great tour through the samples at https://blogs.msdn.com/b/pfxteam/archive/2009/12/09/9934811.aspx.)
The DiningPhilosophers sample uses Tasks. If you’re not familiar with the Dining Philosophers problem, in summary, there are five philosophers sitting at a round dinner table. Each philosopher can do two things: eat or think (and they never do them at the same time). A fork or chopstick is placed between each pair of philosophers, so each philosopher has a utensil to his left and to his right. In this problem, we assume that the dinner is spaghetti, so the philosophers need two forks to properly eat (scooping the spaghetti from the bowl…that is why the problem sometimes uses chopsticks instead of forks; it’s more obvious that you need two chopsticks to eat). This problem illustrates the issue of deadlock, which can easily occur when every philosopher picks up the fork to his left and waits forever for the fork to his right to be available (or vice versa).
In the code sample, in MainWindow.xaml.cs, there are 3 algorithms for running the problem:
RunWithSemaphoresSyncWithOrderedForks
RunWithSemaphoresSyncWithWaitAll
RunWithSemaphoresAsync
Comment out all but one of these, and run the code.
The circles represent the philosophers at the round table. You will see the philosophers change colors as they switch between thinking, eating, and waiting for forks. To decode the colors:
Yellow – the philosopher is thinking
Green – the philosopher is eating
Red – the philosopher is waiting for forks
In the individual methods, you will see the use of StartNew() to start the various thinking, eating, and waiting tasks. You can play with the different algorithms as well.
For more information on Tasks, there is great documentation at https://msdn.microsoft.com/en-us/library/dd537609.aspx.
Stay tuned for tomorrow’s post on the tooling support for parallel programming in Visual Studio 2010.
Comments
Anonymous
June 23, 2010
The comment has been removedAnonymous
June 23, 2010
Thanks Mark!Anonymous
June 23, 2010
Very nice! I've been trying to find time to get aquainted with the new parallel programming support. Old-school threading can be such a chore!Anonymous
October 26, 2010
How do you avoid deadlock issues with SQL Server when using Tasks. I have a method that deletes database rows and inserts new ones during a recalculation process. SQL Server cannot handle the number of tasks coming in so I get a deadlock exception from my DAL. Here is a sample of my code... My goal was to increase performance of my import process by being able to recalculate more claims at once on multiple cores instead of doing them in linear order. Task.Factory.StartNew(() => { foreach (int claimID in recalcs.Keys) { if (DAL.RecalculateClaim(claimID)) ++result; } }).ContinueWith(_ => { Console.WriteLine("Result", result); });Anonymous
November 11, 2010
Ryan - near as I can tell, you're not getting any parallelism out of this. You're starting a single task that has within it a serial foreach loop. To get any parallelism, you'd need to use a parallel foreach, and you'd need to make sure each instance of the delegate got a separate instance of the database access layer code. Database access stuff is almost never thread-safe, and you can't use shared state in parallel tasks.