In the previous article, we started analyzing asynchronous programming in the .NET world. There, we made concerns about how this concept is somewhat misunderstood even though it has been around for more than six years, i.e., since .NET 4.5. Using this programming style, it is easier to write responsive applications that do asynchronous, non-blocking I/O operations. This is done by using async
/await
operators.
However, this concept is often misused. In this article, we will go through some of the most common mistakes using asynchronous programming and give you some guidelines. We will dive into the threading a bit and discuss best practices as well. This should be a fun ride, so buckle up!
Async Void
While reading the previous article, you could notice that methods marked with async
could return either Task, Task<T>
or any type that has an accessible GetAwaiter
method as a result. Well, that is a bit misleading, because these methods can, in fact, return void
, as well. However, this is one of the bad practices that we want to avoid, so we kinda pushed it under the rug. Why is this a misuse of the concept? Well, although it is possible to return void in async
methods, the purpose of these methods is completely different. To be more exact, these kind of methods have a very specific task and that is – making asynchronous handlers possible.
While it is possible to have event handlers that return some actual type, it doesn’t really work well with the language and this notion doesn’t make much sense. Apart from that, some semantics of async void
methods are different from async Task
or async Task<T>
methods. For example, exception handling is not the same. If an exception is thrown in async Task
method, it will be captured and placed within Task
object. If an exception is thrown inside an async void
method, it will be raised directly on the SynchronizationContext that was active.
private async void ThrowExceptionAsync()
{
throw new Exception("Async exception");
}
public void AsyncVoidExceptions_CannotBeCaughtByCatch()
{
try
{
ThrowExceptionAsync();
}
catch (Exception)
{
throw;
}
}
There are two more disadvantages in using async void
. First one is that these methods don’t provide an easy way to notify calling code that they’ve completed. Also, because of this first flaw, it is very hard to test them. Unit testing frameworks, such as xUnit or NUnit, work only for async
methods that are returning Task
or Task<T>
. Taking all this into consideration, using async void
is, in general, frowned upon and using async Task
instead is suggested. The only exception might be in case of asynchronous event handlers, which must return void
.
There is no Thread
Probably the biggest misconceptions about the asynchronous mechanism in .NET is that there is some sort of async thread running in the background. Although it seems quite logical that when you are awaiting some operation, there is the thread that is doing the wait, that is not the case. In order to understand this, let’s take few giant steps back. When we are using our computer, we are having multiple programs running at the same time, which is achieved by running instructions from the different process one at a time on the CPU.
Since these instructions are interleaved and CPU switches from one to another rapidly (context switch), we get an illusion that they are running at the same time. This process is called concurrency. Now, when we are having multiple cores in our CPU, we are able to run multiple streams of these instructions on each core. This is called parallelism. Now, it is important to understand that both of these concepts are available on the CPU level. On the OS level, we have a concept of threads – a sequence of instructions that can be managed independently by a scheduler.
So, why am I giving you lecture from Computer Science 101? Well, because the wait, we were talking about few moments before is happening on the level where the notion of threads is not existing yet. Let’s take a look at this part of the code, generic write operation to a device (network, file, etc.):
public async Task WriteMyDeviceAcync
{
byte[] data = ...
myDevice.WriteAsync(data, 0, data.Length);
}
Now, let’s go down the rabbit hole. WriteAsync
will start overlapped I/O operation on the device’s underlying HANDLE. After that, OS will call the device driver and ask it to start write operation. That is done in two steps. Firstly, the write request object is created – I/O Request Packet or IRP. Then, once device driver receives IRP, it issues a command to the actual device to write the data. There is one important fact here, the device driver is not allowed to block while processing IRP, not even for synchronous operations.
This makes sense since this driver can get other requests too, and it shouldn’t be a bottleneck. Since there is not much more than it can do, device driver marks IRP as “pending” and returns it to the OS. IRP is now “pending”, so OS returns to WriteAsync
. This method returns an incomplete task to the WriteMyDeviceAcync
, which suspends the async
method, and the calling thread continues executing.
After some time, device finishes writing, it sends a notification to the CPU and magic starts happening. That is done via an interrupt, which is at CPU-level event that will take control of the CPU. The device driver has to answer on this interrupt and it is doing so in ISR – Interrupt Service Routine. ISR in return is queuing something called Deferred Procedure Call (DCP), which is processed by the CPU once it is done with the interrupts.
DCP will mark the IRP as “complete” on the OS level, and OS schedules Asynchronous Procedure Call (APC) to the thread that owns the HANDLE. Then I/O thread pool thread is borrowed briefly to execute the APC, which notifies the task is complete. UI context will capture this and knows how to resume.
Notice how instructions that are handling the wait – ISR and DCP are executed on the CPU directly, “below” the OS and “below” the existence of the threads. In an essence, there is no thread, not on OS level and not on device driver level, that is handling asynchronous mechanism.
Foreach and Properties
One of the common errors is using await
inside of foreach
loop. Take a look at this example:
var listOfInts = new List<int>() { 1, 2, 3 };
foreach (var integer in listOfInts)
{
await WaitThreeSeconds(integer);
}
Now, even though this code is written in an asynchronous manner, it will block executing of the flow everytime WaitThreeSeconds
is awaited. This is a real-world situation, for example, WaitThreeSeconds
is calling some sort of the Web API, let’s say it performs an HTTP GET
request passing data for a query. Sometimes, we have situations where we want to do that, but if we implement it like this, we will wait for each request-response cycle to be completed before we start a new one. That is inefficient.
Here is our WaitThreeSeconds
function:
private async Task WaitThreeSeconds(int param)
{
Console.WriteLine($"{param} started ------ ({DateTime.Now:hh:mm:ss}) ---");
await Task.Delay(3000);
Console.WriteLine($"{ param} finished ------({ DateTime.Now:hh: mm: ss}) ---");
}
If we try to run this code, we will get something like this:
Which is nine seconds to execute this code. As mentioned before, it is highly inefficient. Usually, we would expect for each of these Tasks
to be fired and everything to be done in parallel (for a little bit more than three seconds).
Now we can modify the code from the above like this:
var listOfInts = new List<int>() { 1, 2, 3 };
var tasks = new List<Task>();
foreach (var integer in listOfInts)
{
var task = WaitThreeSeconds(integer);
tasks.Add(task);
}
await Task.WhenAll(tasks);
When we run it, we will get something like this:
That is exactly what we wanted. If we want to write it with less code, we can use LINQ:
var tasks = new List<int>() { 1, 2, 3 }.Select(WaitThreeSeconds);
await Task.WhenAll(tasks);
This code is returning the same result and it is doing what we wanted.
And yes, I saw examples where engineers were using async
/await
in the property indirectly since you cannot use async
/await
directly on the property. It is a rather weird thing to do, and I try to stay as far as I can from this antipattern.
Async All the Way
Asynchronous code is sometimes compared to a zombie virus. It is spreading through the code from highest levels of abstractions to the lowest levels of abstraction. This is because the asynchronous code works best when it is called from a piece of another asynchronous code. As a general guideline, you shouldn’t mix synchronous and asynchronous code and that is what “Async all the way” stands for. There are two common mistakes that lie within sync/async code mix:
- Blocking in asynchronous code
- Making asynchronous wrappers for synchronous methods
First one is definitely one of the most common mistakes, that will lead to the deadlock. Apart from that, blocking in an async
method is taking up threads that could be better used elsewhere. For example, in ASP.NET context, this would mean that thread cannot service other requests, while in GUI context, this would mean that thread cannot be used for rendering. Let’s take a look at this piece of code:
public async Task InitiateWaitTask()
{
var delayTask = WaitAsync();
delayTask.Wait();
}
private static async Task WaitAsync()
{
await Task.Delay(1000);
}
Why this code can deadlock? Well, that is one long story about SynchronizationContext, which is used for capturing the context of the running thread. To be more exact, when incomplete Task
is awaited, the current context of the thread is stored and used later when the Task
is finished. This context is the current SynchronizationContext, i.e., current abstraction of the threading within an application. GUI and ASP.NET applications have a SynchronizationContext
that permits only one chunk of code to run at a time. However, ASP.NET Core applications don’t have a SynchronizationContext
so they will not deadlock. To sum it up, you shouldn’t block asynchronous code.
Today, a lot of APIs have pairs of asynchronous and methods, for example, Start()
and StartAsync()
, Read()
and ReadAsync()
. We may be tempted to create these in our own purely synchronous library, but the fact is that we probably shouldn’t. As Stephen Toub perfectly described in his blog post, if a developer wants to achieve responsiveness or parallelism with synchronous API, they can simply wrap the invocation with Task.Run()
. There is no need for us to do that in our API.
Conclusion
To sum it up, when you are using asynchronous mechanism, try to avoid using the async void
methods, except in the special cases of asynchronous event handlers. Keep in mind that there are no extra threads spawned during async
/await
and that this mechanism is done on the lower level. Apart from that, try not to use await
in the foreach
loops and in properties, that just doesn’t make sense. And yes, don’t mix synchronous and asynchronous code, it will give you horrible headaches.
Thank you for reading!