Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Asynchronous Programming in .NET – Task-based Asynchronous Pattern (TAP)

2.82/5 (8 votes)
5 Jun 2018CPOL8 min read 12K  
Task based asynchronous pattern in .NET
In this article, we will take a look at what async patterns are, followed by a discussion of TAP pattern and how to parallelize with TAP pattern.

In the previous articles, we could see what the motivation behind asynchronous programming in .NET is, and we explored some of the most common pitfalls and guidelines when using this programming style. As we mentioned before, async/await mechanism was first introduced in .NET 4.5, but there were other attempts at asynchronous programming before that. To be more specific, two patterns were used for achieving asynchronicity before the aforementioned version of .NET: Asynchronous Programming Model pattern (APM) and Event-based Asynchronous Pattern (EAP). These patterns shouldn’t be used no more, but they are a nice trip down history lane.

Async Patterns

Image 1

For example, if we use APM for developing our asynchronous mechanism, our class would have to implement IAsyncResult interface – meaning, for our asynchronous operation we would need to implement two methods: BeginOperationName and EndOperationName. After BeginOperationName has been called, an application can continue executing operations on the calling thread, and asynchronous operation will be run in a new thread. The result of the asynchronous operation calling thread would get by invoking the EndOperationName method.

On the other hand, a class that supports EAP will have one or more methods that are called MethodNameAsync. These methods are invoked when some Event is received. These classes should also have MethodNameCompleted and MethodNameAsyncCancel methods which are pretty self-explanatory. However, both of these patterns use threads, which is avoided when we use async/await mechanism. Apart from that, we can also notice that they are heavily reliant on naming conventions and developers to synchronize all these threads, which is error-prone.

That is why since .NET version 4.5 we can use Task-based Asynchronous Pattern or TAP, which uses Task, Task and ValueTask (since C# 7) for asynchronous operations. While EAP and APM are still used in the legacy code they are no longer recommended, and TAP should be used instead.

TAP Pattern

Image 2

If you take a closer look, we already talked about the TAP pattern in previous articles. In essence, it is a formalization of the things we are already familiar with plus a little bit more. TAP pattern, unlike APM and EAP, defines only one async method, which can return Task, Task or ValueTask. Also, we can create them without async, but then we need to take care of Task lifecycle manually. These methods are usually named with suffix Async. Apart from that, these methods can either do a small amount of work synchronously or call other async methods by using await. Effectively this means that you can chain these methods.

C#
// Without async
public Task<int> OperationAsync(string input)
{
    // some code
    return new Task<int>().Start();
}

// With async
public async Task<int> MethodAsyncInternal(string input)
{
   // code that uses await
   return value;
}

One of the benefits of using Task as a return type is definitely that you can get the status of it. Since whole asynchronous lifecycle is encapsulated in this Task class, states of that lifecycle are represented by TaskStatus enumeration. There are essentially two ways that Tasks can be in your TAP method: by using public Task constructor using await. When using the public constructor, Tasks will initially be in the Created state and you have to schedule them manually by calling the Start method. These tasks are referred to as “cold tasks”. Otherwise, our Tasks will be initiated and will skip that Created state. One way or another all Tasks returned from TAP method should be activated.

Another benefit of using Task is that we can use cancellation mechanism. In order for TAP method to be cancellable, it has to accept a CancellationToken as a parameter, usually named as cancellationToken. This is the preferred approach. It looks something like this:

C#
public async Task<string> OperationAsync(string param1, int param2, int param3, 
                      CancellationToken cancellationToken)

Now, the cancellation request may come from other parts of the application. When this request is received, the asynchronous operation can cancel the entire operation. TAP method will return Task that is in the Canceled state, which is considered to be the final state for the task. It is important to notice that no exception has been thrown by default.

C#
var token = new CancellationTokenSource();  
string result = await OperationAsync(param1, param2, param3, token.Token);  
… // at some point later, potentially on another thread  
token.Cancel();

Another cool thing that we can do when we use TAP is to get progress notifications of each asynchronous operation. This is handled by passing interface IProgress as a parameter into an asynchronous method. Usually, this parameter is called progress. IProgress interface supports different implementations of progress, as such, which is driven by the application needs. There is default implementation of the IProgress interface – Progress.

C#
public class Progress<T> : IProgress<T>  
{  
    public Progress();  
    public Progress(Action<T> handler);  
    protected virtual void OnReport(T value);  
    public event EventHandler<T> ProgressChanged;  
}

An instance of this class exposes ProgressChanged event. This event would be raised every time asynchronous operation reports a progress. The event is raised on SynchronizationContext on which instance of Progress was created. If the context is not available, a default context that targets the thread pool is used. Handler for this event is passed into the constructor of the Progress class. Progress updates are raised asynchronously as well.

This is how it is used in the code:

var progress = new Progress<int>(ReportProgress);
int uploadedFiles = await UploadFilesAsync(listOfFiles, progress);

Here we constructed Progress object, which will call ReportProgress method every time progress is updated. Then that object is passed into UploadFilesAsync method which should upload a bunch of files, passed as a parameter listOfFiles. Progress should be changed every time file is uploaded. Here is how that might look like:

C#
async Task<int> UploadFilesAsync(List<File> listOfFiles, IProgress<int> progress)
{
    int processCount = await Task.Run<int>(() =>
    {
        int tempCount = 0;
        
        foreach (var file in listOfFiles)
        {
            //await the uploading logic here
            int processed = await UploadFileAsync(file);
            
            if (progress != null)
            {
                progress.Report(tempCount);
            }
            
            tempCount++;
        }

        return tempCount;
    });
    
    return processCount;
}

For each uploaded file in the file list, the Report method with the number of uploaded files would be initiated. This would raise an event, which will be picked up by a handler. In our case, that handler is the ReportProgress method passed into the constructor of Progress class.

Parallelizing with TAP

Image 3

However, the biggest benefit of using TAP or async/await in general is the possibility of the creation of different workflows within the same method. What this means is that we can combine multiple Tasks in several ways, and by doing so, we are changing the flow of our method and our application. For example, consider that you have this situation in your application:

Image 4

This would mean that tasks would be executed sequentially. The second task would run only after the first one is finished, and the third task would run only after the second one is finished. How can we model this in our method? Take a look at this piece of code:

C#
async Task RunWorkflow()
{
    var stopwatch = Stopwatch.StartNew();

    // Task 1 takes 2 seconds to be done.
    await Task.Delay(2000)
        .ContinueWith(task => Completed("Task 1", stopwatch.Elapsed));

    // Task 2 takes 3 seconds to be done.
    await Task.Delay(3000)
        .ContinueWith(task => Completed("Task 2", stopwatch.Elapsed));

    // Task 3 takes 1 second to be done.
    await Task.Delay(1000)
        .ContinueWith(task => Completed("Task 3", stopwatch.Elapsed));

    // Print the final result.
    Completed("Workflow: ", stopwatch.Elapsed);
    stopwatch.Stop();
}

void Completed(string name, TimeSpan time)
{
    Console.WriteLine($"{name} : {time}");
}

So, in here, we assumed that Task 1 would take two seconds to finish the job, Task 2 would take three seconds and finally Task 3 would take one second to be done. After that, we used the ContinueWith method, which is basically creating another task which will execute asynchronously once the Task is done. Also, keep in mind that I used .NET Core 2.0 for this application. It is important to use .NET Core or .NET Framework 4.7 in order for this example to work. The result looks like something like this:

Image 5

There it is, we achieved sequential workflow. Now, this is something we don’t usually don’t want to happen in our application. More often we want our application to run multiple parallel tasks and continue with execution only when they are all done, such as seen on example:

Image 6

In order to make this work, we can exploit the WhenAll method of the Task class, that we had the chance to encounter in the previous blog post. That would look like something like this:

C#
async Task RunWorkflow()
{
    var stopwatch = Stopwatch.StartNew();

    // Task 1 takes 2 seconds to be done.
    var task1 =  Task.Delay(2000)
        .ContinueWith(task => Completed("Task 1", stopwatch.Elapsed));

    // Task 2 takes 3 seconds to be done.
    var task2 = Task.Delay(3000)
        .ContinueWith(task => Completed("Task 2", stopwatch.Elapsed));

    // Task 3 takes 1 second to be done.
    var task3 = Task.Delay(1000)
        .ContinueWith(task => Completed("Task 3", stopwatch.Elapsed));

    await Task.WhenAll(task1, task2, task3);
    
    // Print the final result.
    Completed("Workflow: ", stopwatch.Elapsed);
    stopwatch.Stop();
}

void Completed(string name, TimeSpan time)
{
    Console.WriteLine($"{name} : {time}");
}

Here, instead of awaiting every Task, we used a different tactic. Firstly we created an instance of every task and then used the WhenAll method of the Task class. This method creates a task that will finish only when all of the defined tasks have finished. Here is how it looks like when we run this code:

Image 7

Now, this is much better, as you can see Tasks run in parallel and the whole workflow lasted as long as operation Task 2. Yet sometimes we want to finish the workflow as soon as the first Task is done. Imagine if you are pulling data from multiple servers and you want to continue processing as soon as the first one responds. We can do that using the Task class as well, or to be more specific, the WhenAny method of the Task class. The workflow would look like something like this:

Image 8

In our case Task 3 will first finish the execution so we want to continue processing as soon as that task is done. The code won’t differ too much from the previous example:

C#
async Task RunWorkflow()
{
    var stopwatch = Stopwatch.StartNew();

    // Task 1 takes 2 seconds to be done.
    var task1 =  Task.Delay(2000)
        .ContinueWith(task => Completed("Task 1", stopwatch.Elapsed));

    // Task 2 takes 3 seconds to be done.
    var task2 = Task.Delay(3000)
        .ContinueWith(task => Completed("Task 2", stopwatch.Elapsed));

    // Task 3 takes 1 second to be done.
    var task3 = Task.Delay(1000)
        .ContinueWith(task => Completed("Task 3", stopwatch.Elapsed));

    await Task.WhenAny(task1, task2, task3);
    
    // Print the final result.
    Completed("Workflow: ", stopwatch.Elapsed);
    stopwatch.Stop();
}

void Completed(string name, TimeSpan time)
{
    Console.WriteLine($"{name} : {time}");
}

In a nutshell, we are just calling the WhenAny method instead of the WhenAll method, which essentially creates a task that will finish when any of the supplied tasks have completed. Running this code will print this on the output:

Image 9

We can see that the output looks a tad weird. Still, it makes much sense. What happened is that Task 3 has finished, which caused our WhenAll task to complete. This, on the other hand, caused our Workflow to be completed and with that our two remaining tasks as well.

You might ask yourself “Well, is there a way to create a workflow that will not cut out those two last tasks?” and that would be a good question. What we would like to see is something like this:

Image 10

This scenario is possible in the cases when we want to do a small amount of processing after each of the different tasks has been finished. This can be achieved again by using the WhenAll method of the Task class and a simple loop:

C#
async Task RunWorkflow()
{
    var stopwatch = Stopwatch.StartNew();

    // Task 1 takes 2 seconds to be done.
    var task1 = Task.Delay(2000)
        .ContinueWith(task => Completed("Task 1", stopwatch.Elapsed));

    // Task 2 takes 3 seconds to be done.
    var task2 = Task.Delay(3000)
        .ContinueWith(task => Completed("Task 2", stopwatch.Elapsed));

    // Task 3 takes 1 second to be done.
    var task3 = Task.Delay(1000)
        .ContinueWith(task => Completed("Task 3", stopwatch.Elapsed));

    var tasks = new List<Task>() { task1, task2, task3 };

    while (tasks.Count > 0)
    {
        var finishedTask = await Task.WhenAny(tasks);
        tasks.Remove(finishedTask);
        Console.WriteLine($"Additional processing after finished Task! 
                          {tasks.Count} more tasks remaining!");
    }

    // Print the final result.
    Completed("Workflow: ", stopwatch.Elapsed);
    stopwatch.Stop();
}

void Completed(string name, TimeSpan time)
{
    Console.WriteLine($"{name} : {time}");
}

So when we run this code we will get this output:

Image 11

Conclusion

In this article, we continued exploring a large topic of asynchronous programming in .NET world. We took a small trip down the memory lane and saw how .NET tried to solve this problem with different patterns, coming up with the final solution in .NET 4. This solution is Task-based Asynchronous Pattern or TAP. While it is mostly a formalization of the stuff we already talked about, it gives us a nice refresher course to what we should and what we shouldn’t do. Apart from that, we could see how we can create different workflows in our application by using Tasks.

Thank you for reading!

History

  • 5th June, 2018: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)