In the previous article, we started analyzing asynchronous programming in .NET world. There we made concerns about how this concept is somewhat misunderstood even though it has been around for more than six years, ie. 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, this 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.

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 the 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.):

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 handeling asynchronous mechanism.

Foreach and Properties

One of the common errors is using await inside of foreach loop. Take a look at this example:

Now, this code is even though it 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:

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:

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 PLINQ:

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:

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, ie. 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 the 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!


Read more posts from the author at Rubik’s Code.


9 comments

      1. You said no thread Is created at the user mode, but infact it is created at the krnel mode where a threadpool maintained at kernel level assigns one thread to compute the task. So indirectly it is actually handled / created.
        Also, this is the case of I/O operations
        What about the task which involves extensive computations? If in that case there is no thread created( from threadpool or new)??
        Please correct if I am wrong.

      2. Hi, thank you for reading!
        Well, in fact, there is no thread created at the kernel level either, it is handled by the interrupt. In an essence, we are talking about the level below OS, which means that “threads” as we know it, doesn’t exist yet. Or the other way to put it, not the thread but the hardware is handling await.
        Hope this clears things up.

  1. Great post, thanks! I like the way you explain the “There is no Thread” part. I had to explain that several time to some people, and now I will send then your link!

    btw the link to Stephen Toub post at the end of your post is completely broken, do an inspect element 😉

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.