Task Parallel Library 101 Using C#

Introduction

Most developers are a bit reluctant when it comes to the practice of parallelism and/or concurrency. Probably, because they have the impression that parallelism is complex, especially when it comes to implementation and debugging. Additionally, probably because developers are most likely familiar with synchronous code style since most applications are written that way.

Nevertheless, Task Parallel Library (TPL) comes to the rescue and knowing at least the basic usage of this library gives a lot of benefits to developers and alike.

In this article, we are going to explore the world of Task Parallel Library of .NET Framework using C#. We are going to tackle the following.

  • Task Parallel Library (TPL) and its benefits
  • Create a Task and Start and Wait to Finish
  • Create a Task with Cancellation
  • Create a Task with Continuation

Task Parallel Library(TPL) and its benefits

Task Parallel Library (TPL), basically provides a higher level of abstraction. Fundamentally, it boils down to a “task” which is equivalent to a thread except that it is more lightweight and comes without the overhead of creating an OS thread. In other words, a task is an easier way to execute something asynchronously and in parallel compare to a thread. The main goal is to give the developers the opportunity to add parallelism and/or concurrency to their applications. Moreover; the library gives the developer an idea of what's happening in the current task such as knowing the status, returned values, did the cancellation happened? , did an exception thrown and/or handled? and continuation. 

Getting Started

TPL is a set of APIs using the System.Threading and System.Threading.Task namespace.

Meaning, anytime dealing with TPL APIs, you need to refer to these namespaces on top of your class filename. 

using System.Threading.Tasks;  

Create a Task and Start and Wait to Finish

In this section, we are going to create on how to create a Task.

Create a task using an action void delegate.

var action = new Action(() =>   
{   
    Task.Delay(5000);   
    Console.WriteLine("Hello Task World");   
});  
  
Task myTask = new Task(action);  
myTask.Start();  
  
myTask.Wait();   

Create a task using an action delegate and passes an argument.

var action = new Action<int>((n) =>   
{  
    for (int i = 0; i < n; i++)  
    {  
        Task.Delay(5000);  
        Console.WriteLine($"{i}");  
    }  
});  
  
Task myTask = new Task(() => { action(500); });  
myTask.Start();  
  
myTask.Wait();   

Create a task using a func delegate.

var func = new Func<int, int>((n) =>   
{  
    int total = 0;  
  
    for (int i = 0; i < n; i++)  
    {  
        Task.Delay(5000);  
  
        total += (int)Math.Pow(i, 2);  
  
        Console.WriteLine($"{total}");  
    }  
  
    return total;  
});  
  
Task<int> myTask = new Task<int>(() => { return func(50); });  
myTask.Start();  
  
myTask.Wait();  
  
Console.WriteLine($"My total {myTask.Result}");  

Create a Task with Cancellation

If you are interested in creating a task alongside a cancellation feature. First, you need to define and instantiate a cancellation token. Then, if the cancellation token object is defined and ready, you can pass it before starting the task. For your method-delegate, it is obvious you need to define the cancellation token as a parameter in order to detect if the cancellation has been requested. Let us an example below.

using System;  
using System.Threading;  
using System.Threading.Tasks;  
  
namespace Console_App_Task_With_Cancellation  
{  
    class Program  
    {  
        private static int Fibonacci(int n, CancellationToken token)  
        {  
            int a = 0;  
            int b = 1;  
  
            for (int i = 0; i < n; i++)  
            {  
                if (token.IsCancellationRequested)  
                {  
                    Console.WriteLine("Getting out of the Fibonacci Sequence ....");  
  
                    return a;  
                }  
  
                int temp = a;  
                a = b;  
                b = temp + b;  
  
                Console.WriteLine($"Current result {a}");  
            }  
  
            return a;  
        }  
  
        static void Main(string[] args)  
        {  
            ConsoleKeyInfo input;  
  
            CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();  
  
            CancellationToken token = cancellationTokenSource.Token;  
  
            Task<int> myFibonnaciTask = null;  
  
            myFibonnaciTask = new Task<int>(() => { return Fibonacci(5000, token); }, token);  
  
            myFibonnaciTask.Start();  
  
            do  
            {  
                while(!Console.KeyAvailable)  
                {  
                    if (myFibonnaciTask.IsCompleted)  
                    {  
                        goto End;  
                    }  
                }    
  
                input = Console.ReadKey(true);  
  
                if (!myFibonnaciTask.IsCompleted & !cancellationTokenSource.IsCancellationRequested)  
                {  
                    cancellationTokenSource.Cancel();  
  
                    Console.WriteLine("Cancelled");  
                }  
  
            } while (input.Key != ConsoleKey.Escape);  
  
End:  
            Console.WriteLine($"Result {myFibonnaciTask.Result}");  
            Console.WriteLine("Program has ended!");  
            Console.ReadKey();  
  
        }  
    }  
}  

The major key points here are the Cancel method and IsCancellationRequested property. The Cancel methods say what it does and IsCancellationRequested property checks whether cancellation has been made. 

Create a Task with Continuation

Let us see first an example, then let's start from that. 

var func = new Func<int, int>((n) =>  
{  
    int total = 0;  
  
    for (int i = 0; i < n; i++)  
    {  
        Task.Delay(5000);  
  
        total += (int)Math.Pow(i, 2);  
  
        Console.WriteLine($"{total}");  
    }  
  
    return total;  
});  
  
Task<int> myTask1 = new Task<int>(() => { return func(50); });  
  
Task<int> myTask2Square =  
    myTask1.ContinueWith((myTask) =>  
    {  
        return (int)Math.Sqrt(myTask.Result);  
    }, TaskContinuationOptions.OnlyOnRanToCompletion);  
  
  
myTask1.Start();  
  
Task.WaitAll(myTask1, myTask2Square);  
  
Console.WriteLine($"First result: {myTask1.Result}");  
  
Console.WriteLine($"second result: {myTask2Square.Result}");  

A few points to mention, the above code sample. ContinueWith method, use this once you have decided to continue with another task. After the previous task completion.

In our example above, I used TaskContinuationOptions.OnlyOnRanToCompletion because I'm expecting it to be completed without any exceptions thrown else I used TaskContinuationOptions.OnlyOnFaulted. 

For complete usage of the TaskContinuationOptions, please see the table below. 

TaskContinuationOption Usage
None When no continuation options are specified.
PreferFairness A hint to a System.Threading.Tasks.TaskScheduler object to schedule tasks.
LongRunning Continuation will be long-running.
AttachedToParent Specifies that the continuation is attached to a parent.
DenyChildAttach The attached child task will not be able to attach to the parent task and will execute instead as a detached child task.
HideScheduler
LazyCancellation Prevents completion of the continuation until the antecedent has completed.
RunContinuationsAsynchronously Continuation task should be run asynchronously
NotOnRanToCompletion The task should not be scheduled if its antecedent ran to completion.
NotOnFaulted The task should not be scheduled if its antecedent threw an unhandled exception.
OnlyOnCanceled Scheduled only if its antecedent was canceled.
NotOnCanceled The task should not be scheduled if its antecedent was canceled.
OnlyOnFaulted Scheduled only if its antecedent threw an unhandled exception.
OnlyOnRanToCompletion Scheduled only if its antecedent ran to completion.
ExecuteSynchronously Continuation task should be executed synchronously.

Create a Task with Continuation and Exception Handling.

using System;  
using System.Threading.Tasks;  
  
namespace Console_App_Task_With_Continuation_2  
{  
    class Program  
    {  
        static Random random = new Random();  
          
        static void Main(string[] args)  
        {  
            Task<int> mytask = new Task<int>(() =>  
            {  
                Task.Delay(5000);  
                int result = (random.Next(1, 100) % 2);  
  
                if (result == 0)  
                {  
                    throw new Exception("Just a random exception");  
                }  
                else  
                {  
                    return random.Next(1, 100);  
                }  
            });  
  
            mytask.ContinueWith((task) =>  
            {  
                Console.WriteLine($"Continue with these method when there is an exception. Message:{task.Exception.Message}");  
  
            }, TaskContinuationOptions.OnlyOnFaulted);  
  
            mytask.ContinueWith((task) =>  
            {  
                Console.WriteLine("Task done");  
  
            }, TaskContinuationOptions.OnlyOnRanToCompletion);  
  
            try  
            {  
                mytask.Start();  
  
                Console.WriteLine($"{mytask.Result}");  
            }  
            catch (AggregateException aex)  
            {  
                foreach (var e in aex.Flatten().InnerExceptions)  
                {  
                    if (e is Exception)  
                    {  
                        Console.WriteLine(e.Message);  
                    }  
                }  
            }  
        }  
    }  
}  

The example code above uses the Random object and checks whether the value is odd or even. Then finally, it either throws an exception or finished the task.

In our case, we have two continuation task which we can call antecedents and set its usage. These two antecedents have a specific job, the first one handled the exception and second handled the successful task execution.

Summary

In this article, we have discussed the following.

  • What's the Task Parallel Library(TPL) and some of its benefits? 
  • Not only that, we have seen the basic usage of Task
  • Usage of a cancellation 
  • Usage of continuation
  • Lastly, continuation and exception handling.

I hope you have enjoyed this article, as I have enjoyed it while writing. You can also find the sample code here at GitHub. Till next time, happy programming! 


Recommended Free Ebook
Similar Articles