Recommended Asynchronous Pattern in .NET

Posted by: Damir Arh , on 11/24/2019, in Category C#
Views: 93477
Abstract: The recommended pattern for asynchronous programming in the .NET framework is the task-based asynchronous programming (TAP). This tutorial gives a brief info about this pattern.

Since version 4, the recommended pattern for asynchronous programming in the .NET framework is task-based asynchronous programming (TAP).

As the name implies, it is based on the Task class introduced with the Task Parallel Library (TPL). A task represents an operation running in the background (either asynchronously or on a different thread):

var backgroundTask = Task.Run(() => DoComplexCalculation(42));

By invoking the Wait method or accessing the Result property on the Task class, the invoking thread can wait for a task:

var result = backgroundTask.Result;

However, this will block the invoking thread in the meantime.

Since C# 5 (and .NET framework 4.5), the await keyword is a better alternative:

var result = await backgroundTask;

The execution of the code following the await call will still continue only after the awaited task completes, but the thread won’t idly wait until that happens. Instead, it will be released to do other work. Once the awaited task completes, the code after it will continue to be executed.

The TPL provides helper methods for orchestrating tasks. The simplest one of them is the ContinueWith method. When used, asynchronous execution can continue with another method passed in as a delegate:

var compositeTask = Task.Run(() => DoComplexCalculation(42))
    .ContinueWith(previous => DoAnotherComplexCalculation(previous.Result));

The same can be achieved in a more readable way, using the await keyword:

var intermediateResult = await Task.Run(() => DoComplexCalculation(42));
var result = DoAnotherComplexCalculation(intermediateResult);

There is an important difference in behavior, though.

The line of code after the awaited task will be executed only if that task completed successfully. The delegate of the ContinueWith method will be executed even if the task ends prematurely as canceled or faulted because of an exception. Some overloads of the ContinueWith method accept an additional parameter specifying under which conditions the continuation should run:

var initialTask = Task.Run(() => DoComplexCalculation(42));

var successfulContinuation = initialTask.ContinueWith(previous =>
    DoAnotherComplexCalculation(previous.Result), 
        TaskContinuationOptions.OnlyOnRanToCompletion);
var failedContinuation = initialTask.ContinueWith(previous =>
    HandleError(previous.Exception), TaskContinuationOptions.OnlyOnFaulted);
var canceledContinuation = initialTask.ContinueWith(previous =>
    HandleCancelation(), TaskContinuationOptions.OnlyOnCanceled);

Since C# 6, the standard error handling mechanisms in the language can be used to achieve the same behavior:

try
{
    var intermediateResult = await Task.Run(() => DoComplexCalculation(42));
    var result = DoAnotherComplexCalculation(intermediateResult);
}
catch (TaskCanceledException)
{
    await HandleCancelation();
}
catch (Exception exception)
{
    await HandleError(exception);
}

Since this code is much easier to read and understand, the ContinueWith method is rarely used today.

If you need to run more than one task concurrently, there are methods available to help you coordinate them. To use these methods, you should start all the tasks immediately and collect references to them, e.g. in an array:

var backgroundTasks = new[]
{
    Task.Run(() => DoComplexCalculation(1)),
    Task.Run(() => DoComplexCalculation(2)),
    Task.Run(() => DoComplexCalculation(3))
};

The static helper methods will allow you to wait for all the tasks to complete, either synchronously or asynchronously:

// wait synchronously
Task.WaitAll(backgroundTasks);
// wait asynchronously
await Task.WhenAll(backgroundTasks);

If you only need one of the methods to complete before continuing, you have a different pair of methods at your disposal:

// wait synchronously
Task.WaitAny(backgroundTasks);
// wait synchronously
await Task.WhenAny(backgroundTasks);

Since tasks can be long running, you might want to be able to cancel them prematurely. To allow this option, pass a cancellation token to the method creating the task. You can use it afterwards to trigger the cancellation:

var tokenSource = new CancellationTokenSource();
var cancellableTask = Task.Run(() =>
{
    for (int i = 0; i < 100; i++)
    {
        if (tokenSource.Token.IsCancellationRequested)
        {
            // clean up before exiting
            tokenSource.Token.ThrowIfCancellationRequested();
        }
        // do long-running processing
    }
    return 42;
}, tokenSource.Token);
// cancel the task
tokenSource.Cancel();
try
{
    await cancellableTask;
}
catch (OperationCanceledException)
{
    // handle cancelation
}

If you are the one implementing the task, you need to add additional code to support cancellation. To actually stop the task early, you need to check the cancellation token in the task and react if a cancellation was requested: do any clean up you might need to do and then call the ThrowIfCancellationRequested method to exit the task. This will throw an OperationCanceledException exception, which can then be handled accordingly in the calling thread.

It might seem redundant to also pass the cancellation token as an argument to the Task.Run method. However, this serves two purposes:

1. If the cancellation has been requested before Task.Run is called, then the task will not run at all. Instead it will be immediately put in the Canceled state.

2. If the cancellation is requested while the task is running, it will ensure that the task will transition to the Canceled state instead of the Faulted state when the OperationCanceledException is thrown by the executing code.

To take full advantage of the task-based asynchronous pattern, it’s important that the APIs in the .NET framework implement this pattern themselves. Since .NET framework 4, asynchronous methods returning tasks have been added to existing classes for I/O operations. They return a Task and can usually be easily identified by the Async postfix in their names.

For example, the FileStream class provides methods for performing operations on a stream asynchronously:

using (FileStream srcStream = new FileStream(srcFile, FileMode.Open),
                  destStream = new FileStream(destFile, FileMode.Create))
{
    await srcStream.CopyToAsync(destStream);
}

In some other cases, new classes have been introduced with such methods. The HttpClient class is a newer alternative to the old WebClient class. It provides asynchronous methods for HTTP network operations:

using (var httpClient = new HttpClient())
{
    var content = await httpClient.GetStringAsync(url);
}

When writing fresh code, the task-based asynchronous pattern should be used whenever possible. The only exception to this should be calls to older asynchronous APIs which don’t have alternatives following this pattern.

This article has been editorially reviewed by Suprotim Agarwal.

Absolutely Awesome Book on C# and .NET

C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.

We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).

Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.

Click here to Explore the Table of Contents or Download Sample Chapters!

What Others Are Reading!
Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+

Author
Damir Arh has many years of experience with software development and maintenance; from complex enterprise software projects to modern consumer-oriented mobile applications. Although he has worked with a wide spectrum of different languages, his favorite language remains C#. In his drive towards better development processes, he is a proponent of Test-driven development, Continuous Integration, and Continuous Deployment. He shares his knowledge by speaking at local user groups and conferences, blogging, and writing articles. He is an awarded Microsoft MVP for .NET since 2012.


Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!