Click here to Skip to main content
15,868,014 members
Articles / Programming Languages / C#

Asynchronous Programming in .NET – Common Mistakes and Best Practices

Rate me:
Please Sign up or sign in to vote.
4.93/5 (50 votes)
28 May 2018CPOL9 min read 42.4K   119   13
In this article, we will go through some of the most common mistakes using asynchronous programming and give you some guidelines.

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

Image 1

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.

C#
private async void ThrowExceptionAsync()
{
  throw new Exception("Async exception");
}

public void AsyncVoidExceptions_CannotBeCaughtByCatch()
{
  try
  {
    ThrowExceptionAsync();
  }
  catch (Exception)
  {
    // The exception is never caught here!
    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

Image 2

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

C#
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

Image 3

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

C#
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:

C#
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:

Image 4

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:

C#
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:

Image 5

That is exactly what we wanted. If we want to write it with less code, we can use LINQ:

C#
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

Image 6

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:

C#
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!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Vega IT Sourcing
Serbia Serbia
Read more at my blog: https://rubikscode.net

Comments and Discussions

 
QuestionThere is no Thread Pin
Pranay Rana20-Jun-18 1:42
professionalPranay Rana20-Jun-18 1:42 
SuggestionForeach and async Pin
Dmitry Mukalov8-Jun-18 2:04
Dmitry Mukalov8-Jun-18 2:04 
QuestionVoid async ? Pin
David Sherwood29-May-18 13:22
David Sherwood29-May-18 13:22 
AnswerRe: Void async ? Pin
wkempf31-May-18 8:12
wkempf31-May-18 8:12 
GeneralRe: Void async ? Pin
David Sherwood31-May-18 11:01
David Sherwood31-May-18 11:01 
GeneralRe: Void async ? Pin
wkempf4-Jun-18 3:09
wkempf4-Jun-18 3:09 
QuestionVoid async ? Pin
David Sherwood29-May-18 13:20
David Sherwood29-May-18 13:20 
QuestionWell Explained, Except for... Pin
Member 1232497929-May-18 10:47
Member 1232497929-May-18 10:47 
AnswerRe: Well Explained, Except for... Pin
wkempf31-May-18 8:02
wkempf31-May-18 8:02 
QuestionWell written, but... Pin
MikaelBorjesson29-May-18 8:00
MikaelBorjesson29-May-18 8:00 
Question5 stars Pin
Marco_BR29-May-18 6:30
Marco_BR29-May-18 6:30 
QuestionGreat content Pin
Bill S28-May-18 5:04
professionalBill S28-May-18 5:04 
GeneralMy vote of 5 Pin
Bill S28-May-18 5:02
professionalBill S28-May-18 5:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.