Getting Started with the .NET Task Parallel Library : Page 2

If you have a multi-core computer, chances are your first CPU does all the work while the others spend most of their time twiddling their electronic thumbs. Learn to unlock the idle power of your underused CPUs to greatly improve the performance of your applications.

by Rod Stephens

Sep 26, 2008

Page 2 of 5

Introducing the Task Parallel Library Techniques for distributing tasks across multiple computers have been around for decades. Unfortunately those techniques have mostly been cumbersome, requiring lots of skill and patience to code correctly.

They also had a fair amount of overhead, so a net performance gain occurred only if the pieces of your problem were relatively large. If you divide an easy task into two trivial subtasks, the overhead of calling to remote computers to calculate the subtasks and then reassembling the results often took longer than just doing the work on a single CPU. (If you're a parent, you probably know that it can take longer to make your kids clean up their rooms than it would to clean them yourself. In that case, however, there's a principle involved.)

In contrast, the multi-core computers that are becoming ubiquitous require much less overhead to make and return calls between cores, which makes multi-core programming far more attractive. And the Task Parallel Library simplifies the process by providing routines that can automatically distribute tasks across the computer's available CPUs quickly and painlessly.

The TPL's methods have relatively little overhead, so you don't pay a huge penalty for splitting up your tasks. If your computer has a single CPU, the library pays a small penalty for breaking the tasks up and running them one at a time. If you have a dual-core system (like I do), TPL spreads the tasks across the two CPUs. If you have a quad-core system, TPL spreads the tasks across the four CPUs. In the future, if you have a network of 19 Cell CPUs scattered around your family room and kitchen, TPL will spread the tasks across those 19 CPUs.

At least that's the theory. This scenario is still a bit down the road so don't be surprised if the details change before it becomes a reality. Getting familiar with using TPL now, however, will help you with any future developments in parallel programming later.

One of TPL's additional advantages is that it automatically balances the load across the available CPUs. For example, suppose you run four tasks without TPL and you assign two to run on one CPU and two to run on a second. If the first two tasks take longer than the second two tasks, the second CPU finishes its work early and then sits there uselessly while your program waits for the first CPU to finish.

TPL automatically prevents that sort of unbalanced scheduling by running tasks on whatever CPU is available for work. In this example, your program uses TPL to start the four tasks but doesn't decide where they execute. TPL runs one task on each CPU. When a CPU finishes its current task, TPL gives it another one. The process continues until all of the tasks complete.

If some task is particularly long, other tasks will run on other CPUs. If some tasks are short, one CPU may run many of them very quickly. In any case, the TPL balances the workload, helping to ensure that none of the CPUs sit around idly while there is work to do.

After you've installed the TPL, start a new Visual Basic or C# project and add a reference to System.Threading. Open the Project menu, select Add Reference, select the .NET tab, and double-click the System.Threading entry.

To make working with the namespace easier, you can add an Imports statement in Visual Basic or a using statement in C# to your code module.

Now you're ready to work with TPL. The following sections describe some of the most useful TPL methods: Parallel.Invoke, Parallel.For, and Parallel.ForEach. They also describe two useful parallel objects, Tasks and Futures, and some simple locking mechanisms provided by the System.Threading namespace.

Parallel.Invoke The Parallel class provides static methods for performing parallel operations. The Parallel.Invoke method takes as parameters a set of System.Action objects that tell the method what tasks to perform. Each action is basically the address of a method to run. Parallel.Invoke launches the actions in parallel, distributing them across your CPUs.

The type System.Action is just a named delegate representing a subroutine that takes no parameters and doesn't return anything—in other words, you can simply pass in the addresses of the subroutines that you want to run.

The Parallel.Invoke routine takes a parameter array (ParamArray in Visual Basic) so you can simply list as many subroutines as you like in the call.

The following code shows how you can use Parallel.Invoke in Visual Basic:

Parallel.Invoke(AddressOf Task1, AddressOf Task2, AddressOf Task3)

What could be easier than that? Actually, the following C# version is syntactically a little shorter:

Parallel.Invoke(Task1, Task2, Task3);

That's all you need to do! And you thought using multiple CPUs was going to be a big chore!

Naturally there are a few details that you need to consider, such as:

If two threads try to access the same variables at the same time, they can interfere with each other.

If two threads try to lock several resources at the same time, they may form a deadlock where neither can make progress because each is waiting for a resource locked by the other.

Many classes are not "thread-safe," so you cannot safely use their properties and methods from multiple threads.

For now, ignore these issues and just admire the elegant simplicity of Parallel.Invoke. As long as the tasks mind their own business, it's amazingly easy.

The Parallel.Invoke routine can also take an array of actions as a parameter. That's useful if you don't know at design time exactly which actions you might need to execute. You can fill an array with the tasks at run time and then execute them all. The following Visual Basic code demonstrates that approach:

The sample program InvokeTasks, which is available with the downloadable code for this article in Visual Basic and C# versions, uses Parallel.Invoke to run three tasks in parallel. In that program, the three tasks simply use Thread.Sleep to sleep for 1, 2, and 3 seconds, respectively. As expected, when the program calls these three routines sequentially (one after another), it takes six seconds to execute. It's a simple program, and if you have a multi-core machine, it provides a simple way to determine whether your code is using more than one core, because when the program uses Parallel.Invoke, it takes only three seconds on my dual-core system.

While the application isn't particularly impressive, the results are. The program can access both my CPUs with very simple code.