Introduction
This is a brief introduction to async
and await
keywords to a normal developer who wants to understand the basics and little insights to internals of how stuff works.
Background
Asynchronous programming is now an essential thing when we develop any application because it avoids waiting in main thread on long running operations such as disk I/O, network operations database access, etc. In normal case, if our program needs something to be done from the results of these long operations, our code is struck until the operation is done.
Using async mechanism, we can just trigger long running operations and can do other tasks. Those long running operations do the job in a different thread and when they complete it, they notify our main code and our code can do the post actions from here. When we refer to our code, it's our main thread which deals with user interface or the thread which primarily processes a web request or service UI. Sometimes, we ourselves write these kind of long running operations.
What is async and await
In simple sense, these are 2 new keywords introduced in .NET 4.5 to easily write asynchronous programs. They work in the method level. Of course, we cannot make classes work in parallel as they are not unit of execution.
Are these keywords known to CLR, the .NET run-time or a wrapper over TPL Task Parallel Library? If they are wrappers, is it good to have language depend on a library written using the same language?
We will try to find out the answer to these questions in this article.
History of .NET async Programming
Threads were there from the very beginning of the .NET framework. They were the wrappers on operating system threads and little difficult to work with. Then more concepts such as background worker, async delegate
and Task Parallel Library came to ease the async programming model. Those came as part of class library. C# language as such doesn't have 'out of the box' support for async programming until the async
and await
keywords are introduced with C# 4.0. Let's see how the async
and await
helps us in async programming by examining each of these methods.
Example
Let's take the below example of finding factorial of first N numbers, if they are completely divisible by 3. We are using console application for simplicity. If we had used a Windows application, we could easily hook into async
event delegate handler and demo the async
features in easily. But that won't help us to learn the language features.
Synchronous Code
public void Main()
{
for (int counter = 1; counter < 5; counter++)
{
if (counter % 3 == 0)
{
WriteFactorial(counter);
}
else
{
Console.WriteLine(counter);
}
}
Console.ReadLine();
}
private void WriteFactorial(int no)
{
int result = FindFactorialWithSimulatedDelay(no);
Console.WriteLine("Factorial of {0} is {1}", no, result);
}
private static int FindFactorialWithSimulatedDelay(int no)
{
int result = 1;
for (int i = 1; i <= no; i++)
{
Thread.Sleep(500);
result = result * i;
}
return result;
}
We can see there is a loop that runs from 1 to 5 using counter variable. It finds whether the current counter value is completely divisible by 3. If so, it writes the factorial. The writing function calculates the factorial by calling FindFactorialWithSimulatedDelay()
method. This method here in the sample is going to put delay to simulate real life workload. In other sense, this is the long running operation.
Easily, we can see that the execution is happening in sequence. The WriteFactorial()
call in loop waits until the factorial is calculated. Why should we wait here? Why can't we move to the next number as there is no dependency between numbers? We can. But what about the Console.WriteLine
statement in WriteFactorial()
. It should wait until the factorial is found. It means we can asynchronously call FindFactorialWithSimulatedDelay()
provided there is a call back to the WriteFactorial()
. When the async invocation happens, the loop can advance counter to next number and call the WriteFactorial()
.
Threading is one way we can achieve it. Since the threading is difficult and needs more knowledge than a common developer, we are using async delegate
s mechanism. Below is the rewrite of WriteFactorial()
method using async delegate
.
Making It async Using Async Delegates
One of the easier methods used earlier was to use Asynchronous Delegate Invocation. It uses the Begin
/End
method call mechanism. Here, the run-time uses a thread from thread pool to execute the code and we can have call backs once it's completed. The below code explains it well which uses Func
delegate.
private void WriteFactorialAsyncUsingDelegate(int facno)
{
Func<int, int> findFact = FindFactorialWithSimulatedDelay;
findFact.BeginInvoke(facno,
(iAsyncresult) =>
{
AsyncResult asyncResult = iAsyncresult as AsyncResult;
Func<int, int> del = asyncResult.AsyncDelegate as Func<int, int>;
int factorial = del.EndInvoke(asyncResult);
Console.WriteLine("Factorial of {0} is {1}", facno, factorial);
},
null);
}
public void Main()
{
for (int counter = 1; counter < 5; counter++)
{
if (counter % 3 == 0)
{
WriteFactorialAsyncUsingDelegate(counter);
}
else
{
Console.WriteLine(counter);
}
}
Console.ReadLine();
}
No change in finding factorial. We simply added new function called WriteFactorialAsyncUsingDelegate()
and modified the Main
to call this method from the loop.
As soon as the BeginInvoke
on findFact delegate
is called, the main thread goes back to the counter loop, then it increments the counter and continues looping. When the factorial is available, the anonymous call back will hit and it will be written into console.
Drawbacks
We don't have direct option to cancel the task. Also, if we want to wait for one or more methods, it's a little difficult.
Also, we can see that the piece of code is not wrapped as object and we need to battle with the IAsyncResult
object to get the result back. TPL solves that problem too, It looks more object oriented. Let's have a look.
Improving async Programming Using TPL
TPL is introduced in .NET 4.0. We can wrap the asynchronous code in a Task
object and execute it. We can wait on one or many tasks to be completed. Can cancel task easily, etc... There is more to it. Below is a rewrite of our Factorial writing code with TPL.
private void WriteFactorialAsyncUsingTask(int no)
{
Task<int> task=Task.Run<int>(() =>
{
int result = FindFactorialWithSimulatedDelay(no);
return result;
});
task.ContinueWith(new Action<Task<int>>((input) =>
{
Console.WriteLine("Factorial of {0} is {1}", no, input.Result);
}));
}
public void Main()
{
for (int counter = 1; counter < 5; counter++)
{
if (counter % 3 == 0)
{
WriteFactorialAsyncUsingTask(counter);
}
else
{
Console.WriteLine(counter);
}
}
Console.ReadLine();
}
Here, we can see that the first task is run, then it's continuing with the next task which is the completed handler which receives notification of first task and writing the result to console.
Drawbacks
Still this is not a language feature. We need to refer to the TPL libraries to get support. The main problem here is the effort to write the completed event handler. Let's see how this can be rewritten using async and await keywords.
The Language Feature async and await
We are going to see how the TPL sample can be rewritten using async
and await
keywords. We decorated the WriteFactorialAsyncUsingAwait
method using async
keyword to denote this function is going to do operations in async
manner and it may contain await
keywords. Without async
, we cannot await.
Then, we are awaiting on the factorial finding function. The moment the await
is encountered during the execution, thread goes to the calling method and resumes execution from there. Here in our case, the counter loop, and takes the next number. The awaited code is executed using TPL as its task. As normal, it takes a thread from the pool and executes it. Once the execution is completed, the statements below the await
will be executed.
Here also, we are not going to change anything in the FindFactorialWithSimulatedDelay()
.
private async Task WriteFactorialAsyncUsingAwait(int facno)
{
int result = await Task.Run(()=> FindFactorialWithSimulatedDelay(facno));
Console.WriteLine("Factorial of {0} is {1}", facno, result);
}
public void Main()
{
for (int counter = 1; counter < 5; counter++)
{
if (counter % 3 == 0)
{
WriteFactorialAsyncUsingAwait(counter);
}
else
{
Console.WriteLine(counter);
}
}
Console.ReadLine();
}
This avoids the needs for extra call back handlers and developers can write the code in a sequential manner.
What is the Relation with Task Parallel Library and async await Keywords
The keywords async
and await
make use of TPL internally. More clearly, we can say async
and await
are syntactic sugar in C# language. Still not clear? In other sense, the .NET runtime doesn't know about async
and await
keywords.
Look at the disassembled code of WriteFactorialAsyncUsingAwait()
. You may use reflector or similar tools to disassemble the assembly.
When Should We Use It?
We can use async and await anytime we are waiting for something, i.e., whenever we are dealing with async scenarios. Examples are file IO, network operations, database operations, etc... This will help us to make our UI responsive.
Points of Interest
The interesting things are whether a language should depend upon a library/class created with it? Should the language know the parallel programming constructs? And finally, should the compiler modify our code which will give some new things when we debug our code?
History
- 6th March, 2016: Initial publication