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.