Sunday, 22 October 2017

C# Async Programming - Tasks for dummies

There are umpteenth articles / blogs / guides about the Task-based Asynchronous Pattern used in C# for asynchronous programming. However, I feel that explanations are often convoluted and difficult to follow for something new to language / programming. This week, I explained the pattern to a graduate following some review comments and couldn't find an easy-to-understand article, so thought to explain it myself here.

This will be a series of blogs. I will try to keep it as simple as possible without compromising on completeness. Starting with one of the most basic concept - "The Task".

What is a Task?
Task is the C# abstraction of an asynchronous operation. In other words, it some series of code that executes asynchronously. It may or may/not return a result. 

How are Tasks created?
Tasks can be created explicitly by creating an object of type Task or Task or implicitly by running an async method. For example, both of these expressions end up resulting a task

var task = Task.Run( () => { … });

Func taskFunction = ( async () => { await foo() } ); taskFunction.Invoke();

Tasks and Threads
There are some differences when tasks are created explicitly or implicitly but let's not go there. 

There is a common misunderstanding that creating a task means running on a new thread. This is not true. Whether or not task creates a new thread depends upon how it is created.

For tasks created using Task Parallel Library, using Task.Run() for instance, a thread is created with the task. Running the following code 

              
Console.WriteLine($"Application Thread ID : {Thread.CurrentThread.ManagedThreadId}");
Task.Run(() =>
{
Thread.Sleep(30);
Console.WriteLine("Inside Task");
Console.WriteLine($"Task Thread ID : {Thread.CurrentThread.ManagedThreadId}");
});

Will produce

Application Thread ID : 2
Back to application Thread ID : 2
Inside Task

Task Thread ID : 3

Tasks created by async methods DO NOT create a new thread. Once once task is blocked, control is shifted to other task that is in ready state. 

For example, the following code 

Console.WriteLine($"Application Thread ID : {Thread.CurrentThread.ManagedThreadId}");
Func localTask = (async () =>{Console.WriteLine("Inside Task");Thread.Sleep(30); 
Console.WriteLine($"Task Thread ID :{Thread.CurrentThread.ManagedThreadId}");
});
localTask.Invoke();               
  
Will produce 

Application Thread ID : 2
Inside Task

Back to application Thread ID : 2
Task Thread ID : 2

Note that the thread Id is the same. This means that tasks, unless created by task parallel library, do not run in parallel. They share the same thread and uses context switching to pass control as tasks are blocked and become available again.

The Await Operator
This brings us nicely to the await operator. In simplest words, the await operator cause context switching. The operator is used when executing code block needs to get a result from a task that is running asynchronously. Calling await will block the current routine, only to be returned when the task it is waiting on has completed.

Conclusion
I hope this post will help people in understanding C# tasks. Some of the key take away from this post
  • Tasks can be explicitly using Task Parallel Library or implicitly using async keyword.
  • Task are not same as threads. Some tasks are created in a new thread - the ones created by TPL for instance. While others are created on the same thread.

No comments: