Understanding C# Async methods

Understanding C# Async methods

Async/Await explained

·

5 min read

`Goal: explain what are async methods and how to properly use them (with code examples).

Introduction

C# Multithreading is an advanced programming topic, it offers a performance boost to an application but can lead to the worst kind of issues: the ones that are not consistently reproducible like deadlocks and race conditions. In Microsoft Asynchronous programming article, "reason about" is used frequently, because that's the hard part about multithreading and asynchronous programming: understanding the impact of execution order variations and making sure no issue arises in each scenario.

There are multiple ways to execute parallel code in C#. Today we will just discuss Async methods.

What are Async Methods?

Microsoft's definition

According to async (C# Reference):

An async method runs synchronously until it reaches its first await expression, at which point the method is suspended until the awaited task is complete. In the meantime, control returns to the caller of the method.

It is an accurate definition, even if it is an async method, not all of it is executed asynchronously.

As for the syntax, async methods can have a return type of void, Task or Task<T>, T is the return type.

Other definitions

While diving a little bit deeper into this subject, "State Machine" is another popular explanation for async methods, however, it doesn't help to understand how async methods work. The funny thing is that I get this question a lot in interviews and I provide the answer: "async methods are state machines" but I proceed to explain...

How Async Methods work

It is not easy to explain async methods without going through a couple of code examples first.

Async Method - The Wrong Way

The code below is not how async methods should be used but try to guess the output before reading the answer!

AsyncProgram program = new();
program.Run();
Console.ReadKey();

public class AsyncProgram
{
    public async void Run()
    {
        Console.WriteLine("Main function Started.");  

        await DoWorkAsync();

        Console.WriteLine("Doing some other work...");
        Console.WriteLine("Main Function Finished.");
    }

    private async Task DoWorkAsync()
    {
        Console.WriteLine("Async Method Started.");
        // Time consuming operation:
        await Task.Delay(500);
        Console.WriteLine("Async Method Finished.");
    }
}
/* Output
Main function Started.
Async Method Started.
Async Method Finished.
Doing some other work...
Main Function Finished.
*/

Even though DoWorkAsync is an async method, it is always executed like a sequential method in this example! This is a common mistake: we are eagerly awaiting the Async method.

If you didn't have the right answer in mind, then this article is for you! feel free to add a reaction and/or a comment and please consider subscribing to my newsletter or following me.

Async Method - The Right Way

To leverage the power of Async Methods, we need to keep a reference to the returned Task and await it when we need it (just before exiting the program):

AsyncProgram program = new();
program.Run();
Console.ReadKey();

public class AsyncProgram
{
    public async void Run()
    {
        Console.WriteLine("Main function Started.");  

        Task task = DoWorkAsync();

        Console.WriteLine("Doing some other work...");

        await task;

        Console.WriteLine("Main Function Finished.");
    }

    private async Task DoWorkAsync()
    {
        Console.WriteLine("Async Method Started.");
        // Time consuming operation:
        await Task.Delay(500);
        Console.WriteLine("Async Method Finished.");
    }
}
/* Output
Main function Started.
Async Method Started.
Doing some other work...
Async Method Finished.
Main Function Finished.
*/

In the example above note that what happens between Task task = DoWorkAsync(); and await task; is now asynchronous. The last line of the async method Console.WriteLine("Async Method Finished."); is executed synchronously.

Async Method - Explained Step-by-Step

Let's try to explain what happens in the async example above with a diagram:

A diagram explaining step-by-step execution of an async method

Now the output order (in green) makes sense. Even if we precede Console.WriteLine("Doing some other work..."); with a time-consuming operation the order of output "3" and "4" will be always respected (the async method does not resume until it's awaited). The only asynchronous operation is:

// Time consuming operation:
        await Task.Delay(500);

Async methods and Tasks

Async methods leverage .Net Tasks to provide their execution state. Tasks represent work that will be done by a thread in the thread pool. Using Tasks and Async methods provides a high-level approach to Asynchronous programming resulting in cleaner and more readable code (by just using async/await) with the abstraction of what happens behind the scenes.

However, Task.Run(() => {/* code to execute */}) and async methods do not have the same outcome since the whole method passed to Task.Run() is executed asynchronously.

Other Mistakes to Avoid

Using ValueTask<TResult> vs Task<TResult>

When an async method needs to return data to the caller, in Async return types (C#) it is sometimes recommended to use ValueTask<TResult> instead of Task<TResult> if TResult is a value type. This is a case of reference types vs value types and memory management (check this article about C# memory management).

"async Task" vs "async void"

Even if an async method has no response to return, it is a good practice to use async Task and not async void, since the latter needs a different completion mechanism to be implemented.

Locks

You are shooting yourself in the foot if you use locks in async methods. Different parts of an async method can be executed in different threads, having locks in such scenarios can lead to poor performance or deadlocks.

Locks can be used with threads and tasks but async methods are an abstraction for threads and tasks. If you find yourself needing to use locks with async methods, you need to look for another approach.

Writing stateful code with async methods

Stateful code means depending on global variables to hold a state. Writing asynchronous code that depends on a state is the reason why locks are needed.

The purpose of Async methods is to write cleaner, simpler and more understandable code. With stateless code, you just give an async method input and asynchronously await the output.

Closing thoughts

Async methods are a different approach to asynchronous programming and, in my opinion, better than using Threads or Tasks. One last piece of advice though, multithreading or asynchronous programming performance gains should be always measured to determine if it is worth sacrificing code readability and testability for.

Did you find this article valuable?

Support Simplified Software Engineering by becoming a sponsor. Any amount is appreciated!