10 min read

Compared to the classic threading model in .NET, Task Parallel Library minimizes the complexity of using threads and provides an abstraction through a set of APIs that help developers focus more on the application program instead of focusing on how the threads will be provisioned.

In this article, we’ll learn how TPL benefits of using traditional threading techniques for concurrency and high performance.

There are several benefits of using TPL over threads:

  • It autoscales the concurrency to a multicore level
  • It autoscales LINQ queries to a multicore level
  • It handles the partitioning of the work and uses ThreadPool where required
  • It is easy to use and reduces the complexity of working with threads directly

This tutorial is an extract from the book, C# 7 and .NET Core 2.0 High Performance, authored by Ovais Mehboob Ahmed Khan.

Creating a task using TPL

TPL APIs are available in the System.Threading and System.Threading.Tasks namespaces. They work around the task, which is a program or a block of code that runs asynchronously. An asynchronous task can be run by calling either the Task.Run or TaskFactory.StartNew methods. When we create a task, we provide a named delegate, anonymous method, or a lambda expression that the task executes.


Here is a code snippet that uses a lambda expression to execute the ExecuteLongRunningTasksmethod using Task.Run:

class Program { static void Main(string[] args) { Task t = Task.Run(()=>ExecuteLongRunningTask(5000)); t.Wait(); } public static void ExecuteLongRunningTask(int millis) { Thread.Sleep(millis); Console.WriteLine("Hello World"); } }

In the preceding code snippet, we have executed the ExecuteLongRunningTask method asynchronously using the Task.Run method. The Task.Run method returns the Task object that can be used to further wait for the asynchronous piece of code to be executed completely before the program ends. To wait for the task, we have used the Wait method.

Alternatively, we can also use the Task.Factory.StartNew method, which is more advanced and provides more options. While calling the Task.Factory.StartNew method, we can specify CancellationToken, TaskCreationOptions, and TaskScheduler to set the state, specify other options, and schedule tasks.

TPL uses multiple cores of the CPU out of the box. When the task is executed using the TPL API, it automatically splits the task into one or more threads and utilizes multiple processors, if they are available. The decision as to how many threads will be created is calculated at runtime by CLR. Whereas a thread only has an affinity to a single processor, running any task on multiple processors needs a proper manual implementation.

Task-based asynchronous pattern (TAP)

When developing any software, it is always good to implement the best practices while designing its architecture. The task-based asynchronous pattern is one of the recommended patterns that can be used when working with TPL. There are, however, a few things to bear in mind while implementing TAP.

Naming convention

The method executing asynchronously should have the naming suffix Async. For example, if the method name starts with ExecuteLongRunningOperation, it should have the suffix Async, with the resulting name of ExecuteLongRunningOperationAsync.

Return type

The method signature should return either a System.Threading.Tasks.Task or System.Threading.Tasks.Task. The task’s return type is equivalent to the method that returns void, whereas TResult is the data type.

Parameters

The out and ref parameters are not allowed as parameters in the method signature. If multiple values need to be returned, tuples or a custom data structure can be used. The method should always return Task or Task, as discussed previously.

Here are a few signatures for both synchronous and asynchronous methods:

Synchronous methodAsynchronous methodVoid Execute();Task ExecuteAsync();List GetCountries();Task> GetCountriesAsync();Tuple GetState(int stateID);Task> GetStateAsync(int stateID);Person GetPerson(int personID);Task GetPersonAsync(int personID);

Exceptions

The asynchronous method should always throw exceptions that are assigned to the returning task. However, the usage errors, such as passing null parameters to the asynchronous method, should be properly handled.

Let’s suppose we want to generate several documents dynamically based on a predefined templates list, where each template populates the placeholders with dynamic values and writes it on the filesystem. We assume that this operation will take a sufficient amount of time to generate a document for each template. Here is a code snippet showing how the exceptions can be handled:

static void Main(string[] args) { List

In the preceding code, we have a GenerateDocumentAsync method that performs a long running operation, such as reading the template from the database, populating placeholders, and writing a document to the filesystem. To automate this process, we used Thread.Sleep to sleep the thread for three seconds and then throw an exception that will be propagated to the calling method.

The Main method loops the templates list and calls the GenerateDocumentAsync method for each template. Each GenerateDocumentAsync method returns a task. When calling an asynchronous method, the exception is actually hidden until the Wait, WaitAll, WhenAll, and other methods are called. In the preceding example, the exception will be thrown once the Task.WaitAll method is called, and will log the exception on the console.

Task status

The task object provides a TaskStatus that is used to know whether the task is executing the method running, has completed the method, has encountered a fault, or whether some other occurrence has taken place. The task initialized using Task.Run initially has the status of Created, but when the Start method is called, its status is changed to Running.

When applying the TAP pattern, all the methods return the Task object, and whether they are using the Task.Run inside, the method body should be activated. That means that the status should be anything other than Created. The TAP pattern ensures the consumer that the task is activated and the starting task is not required.

Task cancellation

Cancellation is an optional thing for TAP-based asynchronous methods. If the method accepts the CancellationToken as the parameter, it can be used by the caller party to cancel a task. However, for a TAP, the cancellation should be properly handled. Here is a basic example showing how cancellation can be implemented:

static void Main(string[] args) { CancellationTokenSource tokenSource = new CancellationTokenSource(); CancellationToken token = tokenSource.Token; Task.Factory.StartNew(() => SaveFileAsync(path, bytes, token)); } static Task SaveFileAsync(string path, byte[] fileBytes, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { Console.WriteLine("Cancellation is requested..."); cancellationToken.ThrowIfCancellationRequested } //Do some file save operation File.WriteAllBytes(path, fileBytes); return Task.FromResult(0); }

In the preceding code, we have a SaveFileAsync method that takes the byte array and the CancellationToken as parameters. In the Main method, we initialize the CancellationTokenSource that can be used to cancel the asynchronous operation later in the program. To test the cancellation scenario, we will just call the Cancel method of the tokenSource after the Task.Factory.StartNew method and the operation will be canceled. Moreover, when the task is canceled, its status is set to Cancelled and the IsCompleted property is set to true.

Task progress reporting

With TPL, we can use the IProgress interface to get real-time progress notifications from the asynchronous operations. This can be used in scenarios where we need to update the user interface or the console app of asynchronous operations. When defining the TAP-based asynchronous methods, defining IProgress in a parameter is optional. We can have overloaded methods that can help consumers to use in the case of specific needs. However, they should only be used if the asynchronous method supports them.  Here is the modified version of SaveFileAsync that updates the user about the real progress:

static void Main(string[] args) { var progressHandler = new Progress(value => { Console.WriteLine(value); }); var progress = progressHandler as IProgress; CancellationTokenSource tokenSource = new CancellationTokenSource(); CancellationToken token = tokenSource.Token; Task.Factory.StartNew(() => SaveFileAsync(path, bytes,
token, progress)); Console.Read(); } static Task SaveFileAsync(string path, byte[] fileBytes, CancellationToken cancellationToken, IProgress progress) { if (cancellationToken.IsCancellationRequested) { progress.Report(“Cancellation is called”); Console.WriteLine(“Cancellation is requested…”); } progress.Report(“Saving File”); File.WriteAllBytes(path, fileBytes); progress.Report(“File Saved”); return Task.FromResult(0); }

Implementing TAP using compilers

Any method that is attributed with the async keyword (for C#) or Async for (Visual Basic) is called an asynchronous method. The async keyword can be applied to a method, anonymous method, or a Lambda expression, and the language compiler can execute that task asynchronously.

Here is a simple implementation of the TAP method using the compiler approach:

static void Main(string[] args) { var t = ExecuteLongRunningOperationAsync(100000); Console.WriteLine(“Called ExecuteLongRunningOperationAsync method,
now waiting for it to complete”); t.Wait(); Console.Read(); } public static async Task ExecuteLongRunningOperationAsync(int millis) { Task t = Task.Factory.StartNew(() => RunLoopAsync(millis)); await t; Console.WriteLine(“Executed RunLoopAsync method”); return 0; } public static void RunLoopAsync(int millis) { Console.WriteLine(“Inside RunLoopAsync method”); for(int i=0;i

In the preceding code, we have the ExecuteLongRunningOperationAsync method, which is implemented as per the compiler approach. It calls the RunLoopAsync that executes a loop for a certain number of milliseconds that is passed in the parameter. The async keyword on the ExecuteLongRunningOperationAsync method actually tells the compiler that this method has to be executed asynchronously, and, once the await statement is reached, the method returns to the Main method that writes the line on a console and waits for the task to be completed. Once the RunLoopAsync is executed, the control comes back to await and starts executing the next statements in the ExecuteLongRunningOperationAsync method.

Implementing TAP with greater control over Task

As we know, that the TPL is centered on the Task and Task objects. We can execute an asynchronous task by calling the Task.Run method and execute a delegate method or a block of code asynchronously and use Wait or other methods on that task.

However, this approach is not always adequate, and there are scenarios where we may have different approaches to executing asynchronous operations, and we may use an Event-based Asynchronous Pattern (EAP) or an Asynchronous Programming Model (APM). To implement TAP principles here, and to get the same control over asynchronous operations executing with different models, we can use the TaskCompletionSource object.

The TaskCompletionSource object is used to create a task that executes an asynchronous operation. When the asynchronous operation completes, we can use the TaskCompletionSource object to set the result, exception, or state of the task.

Here is a basic example that executes the ExecuteTask method that returns Task, where the ExecuteTask method uses the TaskCompletionSource object to wrap the response as a Task and executes the ExecuteLongRunningTask through the Task.StartNew method:

static void Main(string[] args) 
{ 
  var t = ExecuteTask(); 
  t.Wait(); 
  Console.Read(); 
} 
 
public static Task ExecuteTask() 
{ 
  var tcs = new TaskCompletionSource(); 
  Task t1 = tcs.Task; 
  Task.Factory.StartNew(() => 
  { 
    try 
    { 
      ExecuteLongRunningTask(10000); 
      tcs.SetResult(1); 
    }catch(Exception ex) 
    { 
      tcs.SetException(ex); 
    } 
  }); 
  return tcs.Task; 
 
} 
 
public static void ExecuteLongRunningTask(int millis) 
{ 
  Thread.Sleep(millis); 
  Console.WriteLine("Executed"); 
} 

So now, we’ve been able to use TPL and TAP over traditional threads, thus improving performance. If you liked this article and would like to learn more such techniques, pick up this book, C# 7 and .NET Core 2.0 High Performance, authored by Ovais Mehboob Ahmed Khan.

Read Next:

Get to know ASP.NET Core Web API [Tutorial]

.NET Core completes move to the new compiler – RyuJIT

Applying Single Responsibility principle from SOLID in .NET Core

I'm a technology enthusiast who designs and creates learning content for IT professionals, in my role as a Category Manager at Packt. I also blog about what's trending in technology and IT. I'm a foodie, an adventure freak, a beard grower and a doggie lover.

LEAVE A REPLY

Please enter your comment!
Please enter your name here