Task Parallelism (Task Parallel Library)

The Task Parallel Library (TPL), as its name implies, is based on the concept of the task. The term task parallelism refers to one or more independent tasks running concurrently. A task represents an asynchronous operation, and in some ways it resembles the creation of a new thread or ThreadPool work item, but at a higher level of abstraction. Tasks provide two primary benefits:

More efficient and more scalable use of system resources.
Behind the scenes, tasks are queued to the ThreadPool, which has been enhanced with algorithms (like hill-climbing) that determine and adjust to the number of threads that maximizes throughput. This makes tasks relatively lightweight, and you can create many of them to enable fine-grained parallelism. To complement this, widely-known work-stealing algorithms are employed to provide load-balancing.

More programmatic control than is possible with a thread or work item.
Tasks and the framework built around them provide a rich set of APIs that support waiting, cancellation, continuations, robust exception handling, detailed status, custom scheduling, and more.

For both of these reasons, in the .NET Framework, tasks are the preferred API for writing multi-threaded, asynchronous, and parallel code.

The Parallel.Invoke method provides a convenient way to run any number of arbitrary statements concurrently. Just pass in an Action delegate for each item of work. The easiest way to create these delegates is to use lambda expressions. The lambda expression can either call a named method, or provide the code inline. The following example shows a basic Invoke call that creates and starts two tasks that run concurrently.

Note

This documentation uses lambda expressions to define delegates in TPL. If you are not familiar with lambda expressions in C# or Visual Basic, see Lambda Expressions in PLINQ and TPL.

The number of Task instances that are created behind the scenes by Invoke is not necessarily equal to the number of delegates that are provided. The TPL may employ various optimizations, especially with large numbers of delegates.

A task is represented by the System.Threading.Tasks.Task class. A task that returns a value is represented by the System.Threading.Tasks.Task<TResult> class, which inherits from Task. The task object handles the infrastructure details, and provides methods and properties that are accessible from the calling thread throughout the lifetime of the task. For example, you can access the Status property of a task at any time to determine whether it has started running, ran to completion, was canceled, or has thrown an exception. The status is represented by a TaskStatus enumeration.

When you create a task, you give it a user delegate that encapsulates the code that the task will execute. The delegate can be expressed as a named delegate, an anonymous method, or a lambda expression. Lambda expressions can contain a call to a named method, as shown in the following example.

// Create a task and supply a user delegate by using a lambda expression. var taskA = new Task(() => Console.WriteLine("Hello from taskA."));
// Start the task.
taskA.Start();
// Output a message from the joining thread.
Console.WriteLine("Hello from the calling thread.");
// Message from taskA should follow. /* Output:
* Hello from the calling thread.
* Hello from taskA.
*/

You can also use the Run methods to create and start a task in one operation. To manage the task, theRun methods use the default task scheduler, regardless of which task scheduler is associated with the current thread. The Run methods are the preferred way to create and start tasks when more control over the creation and scheduling of the task is not needed.

You can also use the StartNew method to create and start a task in one operation. Use this method when creation and scheduling do not have to be separated and you require additional task creation options or the use of a specific scheduler, or when you need to pass additional state into the task through its AsyncState property, as shown in the following example.

// Create and start the task in one operation. var taskA = Task.Factory.StartNew(() => Console.WriteLine("Hello from taskA."));
// Output a message from the joining thread.
Console.WriteLine("Hello from the joining thread.");

Task and Task<TResult> each expose a static Factory property that returns a default instance of TaskFactory, so that you can call the method as Task.Factory.StartNew(). Also, in this example, because the tasks are of type System.Threading.Tasks.Task<TResult>, they each have a public Result property that contains the result of the computation. The tasks run asynchronously and may complete in any order. If Result is accessed before the computation finishes, the property will block the thread until the value is available.

When you use a lambda expression to create a delegate, you have access to all the variables that are visible at that point in your source code. However, in some cases, most notably within loops, a lambda doesn’t capture the variable as expected. It only captures the final value, not the value as it mutates after each iteration. You can access the value on each iteration by providing a state object to a task through its constructor, as shown in the following example:

This state is passed as an argument to the task delegate, and it can be accessed from the task object by using the AsyncState property. Also, passing in data through the constructor might provide a small performance benefit in some scenarios.

Every task receives an integer ID that uniquely identifies it in an application domain and can be accessed by using the Id property. The ID is useful for viewing task information in the Visual Studio debugger Parallel Stacks and Parallel Tasks windows. The ID is lazily created, which means that it isn’t created until it is requested; therefore, a task may have a different ID every time that the program is run. For more information about how to view Task IDs in the debugger, see Using the Parallel Stacks Window and Using the Parallel Stacks Window.

Most APIs that create tasks provide overloads that accept a TaskCreationOptions parameter. By specifying one of these options, you tell the task scheduler how to schedule the task on the thread pool. The following table lists the various task creation options.

Element

Description

None

The default option when no option is specified. The scheduler uses its default heuristics to schedule the task.

PreferFairness

Specifies that the task should be scheduled so that tasks created sooner will be more likely to be executed sooner, and tasks created later will be more likely to execute later.

LongRunning

Specifies that the task represents a long-running operation.

AttachedToParent

Specifies that a task should be created as an attached child of the current task, if one exists. For more information, see Nested Tasks and Child Tasks.

DenyChildAttach

Specifies that if an inner task specifies the AttachedToParent option, that task will not become an attached child task.

The Task.ContinueWith method and Task<TResult>.ContinueWith method let you specify a task to be started when the antecedent task finishes. The delegate of the continuation task is passed a reference to the antecedent, so that it can examine its status. In addition, a user-defined value can be passed from the antecedent to its continuation in the Result property, so that the output of the antecedent can serve as input for the continuation. In the following example, getData is started by the program code. Then, analyzeData is started automatically when getData finishes, and reportData is started when analyzeData finishes. getData produces as its result a byte array, which is passed into analyzeData. analyzeData processes that array and returns a result whose type is inferred from the return type of the Analyze method. reportData takes the input from analyzeData, and produces a result whose type is inferred in a similar manner and which is made available to the program in the Result property.

When user code that is running in a task creates a new task and does not specify the AttachedToParent option, the new task is not synchronized with the outer task in any special way. Such tasks are called a detached nested task. The following example shows a task that creates one detached nested task.

When user code that is running in a task creates a task with the AttachedToParent option, the new task is known as a child task of the originating task, which is known as the parent task. You can use the AttachedToParent option to express structured task parallelism, because the parent task implicitly waits for all child tasks to finish. The following example shows a task that creates one child task:

Some overloads let you specify a time-out, and others take an additional CancellationToken as an input parameter, so that the wait itself can be canceled either programmatically or in response to user input.

When you wait for a task, you implicitly wait for all children of that task that were created by using the TaskCreationOptionsAttachedToParent option. Task.Wait returns immediately if the task has already completed. Any exceptions raised by a task will be thrown by a Wait method, even if the Wait method was called after the task completed.

The Task and Task<TResult> classes provide several methods that can help you compose multiple tasks to implement common patterns and to better use the asynchronous language features that are provided by C#, Visual Basic, and F#. This section describes the WhenAll, WhenAny, Delay, and FromResult<TResult> methods.

The Task.WhenAll method asynchronously waits for multiple Task or Task<TResult> objects to finish. It provides overloaded versions that enable you to wait for non-uniform sets of tasks. For example, you can wait for multiple Task and Task<TResult>, objects to complete from one method call. The following basic example uses Task.WhenAll to create a task that represents the completion of three other tasks.

The Task.WhenAny method asynchronously waits for one of multiple Task or Task<TResult> objects to finish. As in the Task.WhenAll method, this method provides overloaded versions that enable you to wait for non-uniform sets of tasks. The WhenAny method is especially useful in the following scenarios.

Redundant operations. Consider an algorithm or operation that can be performed in many ways. You can use the WhenAny method to select the operation that finishes first and then cancel the remaining operations.

Interleaved operations. You can start multiple operations that must all finish and use the WhenAny method to process results as each operation finishes. After one operation finishes, you can start one or more additional tasks.

Throttled operations. You can use the WhenAny method to extend the previous scenario by limiting the number of concurrent operations.

Expired operations. You can use the WhenAny method to select between one or more tasks and a task that finishes after a specific time, such as a task that is returned by the Delay method. The Delay method is described in the following section.

The following basic example uses Task.WhenAny to select the first task to finish out of three tasks.

The Task.Delay method produces a Task object that finishes after the specified time. You can use this method to build loops that occasionally poll for data, introduce time-outs, delay the handling of user input for a predetermined time, and so on. The following basic example creates a continuation task that finishes when one of two lengthy operations complete, or a one-second time-out expires.

When a task throws one or more exceptions, the exceptions are wrapped in a AggregateException. That exception is propagated back to the thread that joins with the task, which is typically the thread that is waiting for the task to finish or accesses the Result property. This behavior serves to enforce the .NET Framework policy that all unhandled exceptions by default should tear down the process. The calling code can handle the exceptions by using the Wait, WaitAll, or WaitAny method or the Result property on the task or group of tasks, and enclosing the Wait method in a try-catch block.

The joining thread can also handle exceptions by accessing the Exception property before the task is garbage-collected. By accessing this property, you prevent the unhandled exception from triggering the exception propagation behavior which tears down the process when the object is finalized.

In some cases, you may want to use a Task to encapsulate some asynchronous operation that is performed by an external component instead of your own user delegate. If the operation is based on the Asynchronous Programming Model Begin/End pattern, you can use the FromAsync methods. If that is not the case, you can use the TaskCompletionSource<TResult> object to wrap the operation in a task and thereby gain some of the benefits of Task programmability, for example, support for exception propagation and continuations. For more information, see TaskCompletionSource<TResult>.

Most application or library developers do not care which processor the task runs on, how it synchronizes its work with other tasks, or how it is scheduled on the System.Threading.ThreadPool. They only require that it execute as efficiently as possible on the host computer. If you require more fine-grained control over the scheduling details, the Task Parallel Library lets you configure some settings on the default task scheduler, and even lets you supply a custom scheduler. For more information, see TaskScheduler.