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
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
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.
public Task<int> OperationAsync(string input)
{
return new Task<int>().Start();
}
public async Task<int> MethodAsyncInternal(string input)
{
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:
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.
var token = new CancellationTokenSource();
string result = await OperationAsync(param1, param2, param3, token.Token);
…
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.
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:
async Task<int> UploadFilesAsync(List<File> listOfFiles, IProgress<int> progress)
{
int processCount = await Task.Run<int>(() =>
{
int tempCount = 0;
foreach (var file in listOfFiles)
{
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
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 Task
s 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:
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:
async Task RunWorkflow()
{
var stopwatch = Stopwatch.StartNew();
await Task.Delay(2000)
.ContinueWith(task => Completed("Task 1", stopwatch.Elapsed));
await Task.Delay(3000)
.ContinueWith(task => Completed("Task 2", stopwatch.Elapsed));
await Task.Delay(1000)
.ContinueWith(task => Completed("Task 3", stopwatch.Elapsed));
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:
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:
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:
async Task RunWorkflow()
{
var stopwatch = Stopwatch.StartNew();
var task1 = Task.Delay(2000)
.ContinueWith(task => Completed("Task 1", stopwatch.Elapsed));
var task2 = Task.Delay(3000)
.ContinueWith(task => Completed("Task 2", stopwatch.Elapsed));
var task3 = Task.Delay(1000)
.ContinueWith(task => Completed("Task 3", stopwatch.Elapsed));
await Task.WhenAll(task1, task2, task3);
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:
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:
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:
async Task RunWorkflow()
{
var stopwatch = Stopwatch.StartNew();
var task1 = Task.Delay(2000)
.ContinueWith(task => Completed("Task 1", stopwatch.Elapsed));
var task2 = Task.Delay(3000)
.ContinueWith(task => Completed("Task 2", stopwatch.Elapsed));
var task3 = Task.Delay(1000)
.ContinueWith(task => Completed("Task 3", stopwatch.Elapsed));
await Task.WhenAny(task1, task2, task3);
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:
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:
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:
async Task RunWorkflow()
{
var stopwatch = Stopwatch.StartNew();
var task1 = Task.Delay(2000)
.ContinueWith(task => Completed("Task 1", stopwatch.Elapsed));
var task2 = Task.Delay(3000)
.ContinueWith(task => Completed("Task 2", stopwatch.Elapsed));
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!");
}
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:
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