The Task Parallel Library (TPL), introduced in .NET 4.0 and refined in 4.5, provides a higher-level abstraction over the thread pool. It shifts focus from raw threads to tasks—units of asynchronous work that may or may not execute on separate threads. A primary strength is its rich set of combinators that simplify chaining and error handling. Exceptions thrown inside tasks are wrapped in AggregateException, letting calllers inspect and handle them individually.
Defining and Starting Tasks
Tasks can be created through several approaches. The preferred modern pattern is Task.Run, which queues work on the thread pool and prevents child tasks from attaching to the parent (via DenyChildAttach).
Task<int> compute = Task.Run(() =>
{
// Simulate work
Thread.Sleep(200);
return 42;
});
An alternative is constructing a Task instance and later calling Start:
var coldTask = new Task<int>(() => 99);
coldTask.Start();
Task.Factory.StartNew offers more detailed control. Passing TaskCreationOptions.LongRunning hints the scheduler to avoid the thread pool, typically creating a dedicated background thread:
var longOp = Task.Factory.StartNew(() =>
{
// long-running CPU or I/O work
}, TaskCreationOptions.LongRunning);
A task can be forced to execute on the calling thread with RunSynchronously, which is useful when the overhead of thread-pool queuing outweighs the operation's cost. The Status property (Created, Running, RanToCompletion, etc.) lets you poll task lifecycles manually, though combining tasks is generally cleaner.
Chaining and Continuations
ContinueWith schedules a delegate that runs after a target task finishes. By default it grabs a new thread-pool worker. Adding TaskContinuationOptions.ExecuteSynchronously changes the scheduling slightly: if the antecedent hasn't completed, the continuation runs on the antecedent's thread; if it has already finished, the continuation executes on the current thread.
Task initial = Task.Run(() => PrepareData());
Task follow = initial.ContinueWith(prev =>
{
Process(prev.Result);
}, TaskContinuationOptions.ExecuteSynchronously);
An alternative hook is GetAwaiter().OnCompleted, which behaves differently depending on completion status: it runs on the antecedent's thread when the task is still in progress, and otherwice on a thread-pool thread.
Parent-child relationships are established with TaskCreationOptions.AttachedToParent. A child must be started while the parent is active. The parent transitions to WaitingForChildrenToComplete and finishes only after all attached children have finished, even if children spawn further nested children.
Bridging Legacy Asynchronous Patterns
APM to Tasks
Task<T>.Factory.FromAsync converts the Asynchronous Programming Model (APM) pattern into a task. Three overloads are commonly used:
// Overload 1: wrapping an already-started IAsyncResult
IAsyncResult ar = someDelegate.BeginInvoke("arg", null, null);
Task<string> t1 = Task<string>.Factory.FromAsync(ar, asyncResult => someDelegate.EndInvoke(asyncResult));
// Overload 2: supplying begin/end methods and arguments
Task<int> t2 = Task<int>.Factory.FromAsync(
(arg, callback, state) => BeginOperation(arg, callback, state),
asyncResult => EndOperation(asyncResult),
"input",
null
);
// Overload 3: using a lambda that captures the IAsyncResult
IAsyncResult ar2 = BeginOtherOp(null);
Task<decimal> t3 = Task<decimal>.Factory.FromAsync(ar2, _ => EndOtherOp(ar2));
EAP to Tasks
The Event-based Asynchronous Pattern (EAP) can be wrapped with TaskCompletionSource<T>. Assign SetResult, SetException, or SetCanceled inside the completion callback. Always embed these calls in a try/catch so that any exception in the callback itself is reflected on the task.
var tcs = new TaskCompletionSource<string>();
someComponent.DownloadCompleted += (sender, args) =>
{
try
{
if (args.Error != null)
tcs.TrySetException(args.Error);
else if (args.Cancelled)
tcs.TrySetCanceled();
else
tcs.TrySetResult(args.Result);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
};
someComponent.StartDownload();
string result = tcs.Task.Result;
Cooperative Cancellation
A CancellationTokenSource paired with a CancellationToken enables clean task cancellation. Cancel before starting and the task transitions immediately to Canceled; calling Start again throws InvalidOperationException. Cancel after the task is running, and the task will likely reach RanToCompletion, possibly after checking the token and terminating early.
var source = new CancellationTokenSource();
CancellationToken token = source.Token;
Task worker = Task.Run(() =>
{
for (int i = 0; i < 100; i++)
{
token.ThrowIfCancellationRequested();
// perform work
}
}, token);
source.CancelAfter(500);
Exception Handling Strategies
- Accessing
Task.Resultblocks and propagates the original exception wrapped insideAggregateException. AggregateException.InnerExceptionyields the first exception; the collection may itself contain otherAggregateExceptioninstances.- Flattening the root
AggregateExceptionwithFlatten()retrieves all leaf-level exceptions across nested aggregates. - Using
GetAwaiter().GetResult()unwraps the original exception. When caught inside atry/catch, the TPL infrastructure surfaces the raw exception without an outerAggregateException. - Fault-only continuations (
TaskContinuationOptions.OnlyOnFaulted) act as specialized exception handlers:
Task.Run(() => PossiblyFaultyWork())
.ContinueWith(faultedTask => Log(faultedTask.Exception),
TaskContinuationOptions.OnlyOnFaulted);
Running Tasks in Parallel
Task.WhenAll returns a task that completes when every supplied task has finished; its result is an array of the individual results. Task.WhenAny finishes as soon as any one of the provided tasks completes, returning the winner.
var tasks = new[] { DownloadAsync(url1), DownloadAsync(url2) };
string[] allResults = await Task.WhenAll(tasks);
Task<int> fastest = await Task.WhenAny(tasks);
int firstResult = await fastest;
Custom Scheduling and UI Threads
The default TaskScheduler puts work on thread-pool threads. For UI frameworks, TaskScheduler.FromCurrentSynchronizationContext() captures the synchronization context, enabling continuations to marshal back to the UI thread.
Task.Run(() => LoadData())
.ContinueWith(dataTask =>
{
// Update UI safely on the captured context
textBox.Text = dataTask.Result;
}, TaskScheduler.FromCurrentSynchronizationContext());
Avoid synchronous blocks on the UI thread, as they can lead to deadlocks. Prefer ContinueWith with the correct scheduler or the async/await pattern, which automatically resumes on the captured context.