DEV Community

Kamil Bugno
Kamil Bugno

Posted on • Updated on

Asynchronous programming in C#

Would you like to find out why asynchronous programming is important, how to use it in C#, and what advantages asynchronous code has? Let's get into it!

Why should we use asynchronous programming?

Most people who describe usage of asynchronism explain it by the example of a desktop app, so I will focus on this topic first.

Desktop app

Let's assume that we have a button called 'Process' in our desktop app, and when a user clicks it some computation that require access to external resources will take place (for example, API or database). If we don't use asynchronism, the entire app will be frozen during the processing time - our user won't be able to interact with the app. The different situation is with the power of asynchronous code. A main thread that is responsible for displaying the app view won't be blocked, and no frozen time will appear.

Let's visualize this situation in a detailed way:
1) On the left we have our desktop app. On the right there are details related to CPU and threads. When the app was rendering a view (UI) for the first time, a thread used some resources to do it (black rectangular element). Later, we can observe that there isn't any other action, and the app is idle. Nothing will happen until a user interact with the UI.

Image description

2) Let's imagine that the user clicked the button. The app started to be active and a thread was used to handle user interaction.
Image description

3) If you remember, we assumed that an action behind this button is to access external API or database. Let's see what will happen:

Image description

4) And here is the most crucial point: when we don't use asynchronous programming, our thread will be actively wait until the end of the API call. During this time, the application will be frozen, and the user won't be able to interact with it:

Image description

5) In an alternative situation with asynchronous programming, you can notice that during external call our CPU is free and the user can easily interact with the app without the frozen time.

Image description

The above situation was described for a desktop app, but let's be honest - this type is not as popular as it used to be. Currently, we usually create some web apps with a backend. So, what effect on it will asynchronous programming have?

Web server app

Let's assume that we have an API written in C#, and one of its endpoints accesses the database to retrieve some data. If we don't use asynchronous programming the thread will be waiting during the database retrieval operation. As a result, when a new request is come to our API, we couldn't process it effectively. To make matters worse, our system can create some additional threads in the background to handle new requests. Why this situation is not good? Because creating new threads is consuming, and when the CPU is busy with our first request, newly created threads won't be scheduled and unfortunately, they will be waiting.

Let's observe this situation on the images:
1) At the beginning we see that a thread was used. The main goal for it was to start the web server, do some start up operations, etc. After that, the server is idle because there aren't any user requests:
Image description

2) Our API received one HTTP request. A thread will take care of handling this request:
Image description

3) During that request we want to get some data from external resource. When we reach the C# code responsible for retrieving data from the database, the situation will be the following:

Image description

4) Without asynchronism we end up with the thread that will do nothing except for waiting for the database query's result. New requests cannot be easily processed.

Image description

5) With asynchronous programming we will have a free CPU that can handle new requests, so our API is more effective - it can process more requests than in the previous situation.

Image description

The web server explanation assumed that we had only one CPU, because I want to keep it as simple as possible, but the situation with several CPU will be quite similar.

Currently, you should know what the benefits of asynchronous programming are. Let's see how we can use it in C#.

Async and await

If we want to use the power of asynchronism, we can easily do it in C# by two keywords: async and await. It is good to know that there are some rules related with these words - let's dig into this.

1. Method that uses await should be async

If we assume that we want to make some HTTP request, we can do it in that way:

public async Task DoSth()
{
     var client = new HttpClient();
     var result = await client.GetAsync("https://www.kamilbugno.com");
     //...
}
Enter fullscreen mode Exit fullscreen mode

Please note that GetAsync is an asynchronous method and thus should be called with an await keyword. Every method that uses await must be marked as async. Without having async in the method declaration, we will receive an error and the code won't compile correctly.

2. It is possible to run async operation without await

The following code will compile even if there is no await keyword:

public async Task DoSth()
{
     var client = new HttpClient();
     var result = client.GetAsync("https://www.kamilbugno.com");
     //...
}
Enter fullscreen mode Exit fullscreen mode

What is more, we can also remove the async word, because there is no await usage in the body of the method. This process is called 'Eliding async and await'. It is good to know, that it has some big consequences and is only recommended in a special situation.

Consequences

The code will simply continue working without taking care of the async operation. It may seem to be an advantage, but there are some issues related to it:

  • using statement: if our code is in a using statement, the object can be disposed before the operation ends, and, as a result, the operation won't succeed.
  • try/catch block: it may happen that the execution reached the end of try/catch block before the operation ends, and, as a result, we will not be able to properly handle the exception.
  • using 'Eliding async and await' incorrectly can even cause a termination of the entire process.

Use Case

Let's assume that we have two nested methods A and B (B is called inside A and contains some async operations). Both methods A and B use async/await word. In that scenario, we can skip async/await in the inner method (B) because it will be eventually awaited in A method. This is the only situation, when considering 'Eliding async and await' might be a good idea.

Example:

public async Task A()
{
    var result = await B();
    //...
}

public Task<HttpResponseMessage> B()
{
    var client = new HttpClient();
    return client.GetAsync("https://www.kamilbugno.com");
}
Enter fullscreen mode Exit fullscreen mode

3. Async method can return Task, Task<T>, ValueTask<T>, or be void

The async method can return several types, and now we will focus on the most common ones:

a) Task vs void

Task is usually used when we don't want to return any real value. Why don't use void? It is true that in non-asynchronous world in this scenario we will use void without a second thought, but in async code it is not valid anymore. Task object contains some additional information about the operation and can be awaitable, so this is the reason why most often you will see Task instead of void. What is more, it is impossible to use await word to call async method that returns void.

b) Task<T> vs ValueTask<T>

When we want to return some value, we have two options Task<T> and ValueTask<T>. What is the difference? The former is a reference type whereas the latter is a value type. ValueTask<T> is in some cases better when it comes to the performance than Task<T>. There is one special scenario for using it: when the execution path can retrieve data in both synchronous and asynchronous way. Sounds complicated? Let's see an example:

public async ValueTask<int> GetPageVersion()
{
     if (_cachedValue == null)
     {
         var client = new HttpClient();
         var result = await client.GetAsync("https://www.kamilbugno.com");
         _cachedValue = result.Version.Build;
     }    
     return _cachedValue.Value;     
}
Enter fullscreen mode Exit fullscreen mode

As you can see, the method GetPageVersion behaves differently based on the cached value. It is likely that most of the time, the asynchronous code will not be run. In that case ValueTask<T> is a better choice. In any other scenarios you should consider Task<T>.

4. Async method can be called in a sync way

It is quite important to be able to distinguish a situation when we have a synchronous and asynchronous code.
Let's see an example:

public int GetPageVersion()
{
     var client = new HttpClient();
     var result = client.GetAsync("https://www.kamilbugno.com").GetAwaiter().GetResult();
     return result.Version.Build;
}
Enter fullscreen mode Exit fullscreen mode

We run client.GetAsync method, and as a name suggest, it is asynchronous method. But the way of calling this method matters: when we use GetAwaiter and GetResult the code will be executed synchronously. It is good to know that using it can be dangerous - we can even end up with deadlocks.

Now, you should know the basics of using async and await in your C# code. But do we really know how it works? Let's explore this topic more deeply.

Under the hood of asynchronism

To be able to go under the hood we will need some Intermediate Language (IL) viewer. When we build the project, the .NET environment generates IL code from our source code and thanks to it we can really see what is going here.

Let's assume that we have a simple class with an empty method. The IL code for it will be the following:
Image description

It isn't very readable, but we can see that in some way it is similar to original method, isn't it? But what will happen when we add async word to the method declaration?

Image description

As you can see, IL code is completely different. Instead of having a method DoSth, we currently have a class. Why? Because async/await words are using a state machine. If you take a close look at the IL code, you can see that DoSth implements interface IAsyncStateMachine, contains property state, and has method MoveNext.

How does state machine work?

It is important that state machine is not an entity that exists only in C# world, or that is related only to asynchronism. State machine is an implementation of the state design pattern, and you can create your own state machine. It can behave differently depending on its internal state. Sounds confusing? Let's imagine that we have following state machine.
Image description

We can be hungry or full - these are the states that our machine has. As you may have guessed, we aren't usually all the time hungry or full, during the day it is possible to change the state. By eating pizza, you can move from hungry to full state, and by going swimming your state can reach hungry.

Analogous situation we have in C# when it comes to asynchronous programming. The previously mentioned MoveNext method is responsible for changing the state of the machine and do some actions.

Let's assume that we have the simple async method:

public async Task<string> DoSth()
{
     Console.WriteLine("one");
     var client = new HttpClient();
     var result = await client.GetStringAsync("https://www.kamilbugno.com");
     Console.WriteLine("two");
     return result;
}
Enter fullscreen mode Exit fullscreen mode

The method MoveNext that is generated for this code can look similar to this:

private void MoveNext()
{
    int num = this.state;
    string result;
    try
    {
        TaskAwaiter<string> awaiter;
        if (num != 0)
        {
            Console.WriteLine("one");
            client = new HttpClient();
            awaiter = client.GetStringAsync("https://www.kamilbugno.com").GetAwaiter();
            if (!awaiter.IsCompleted)
            {
                num = (state = 0);
                this.taskAwaiter = awaiter;
                this.stateMachine = this;
                this.asyncTaskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
                return;
            }
        }
        else
        {
            awaiter = this.taskAwaiter;
            this.taskAwaiter = default(TaskAwaiter<string>);
            num = (state = -1);
        }
        this.myString = awaiter.GetResult();
        this.result = this.myString;
        this.myString = null;
        Console.WriteLine("two");
        result = this.result;
    }
    catch (Exception exception)
    {
        state = -2;
        client = null;
        this.result = null;
        this.builder.SetException(exception);
        return;
    }
    state = -2;
    this.client = null;
    this.result = null;
    this.builder.SetResult(result);
}
Enter fullscreen mode Exit fullscreen mode

I changed some variable names to make it more readable. The flow of this state machine is the following:
1) At the beginning we have this.state set to -1.
2) MoveNext method is called. Because of the state value the if statement is true, so we execute this piece of code:

Console.WriteLine("one");
client = new HttpClient();
awaiter = client.GetStringAsync("https://www.kamilbugno.com").GetAwaiter();
Enter fullscreen mode Exit fullscreen mode

Let's assume that the awaiter is not completed so we set the state to 0 and exit the method:

num = (state = 0); 
this.taskAwaiter = awaiter; 
this.stateMachine = this;
this.asyncTaskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
return;
Enter fullscreen mode Exit fullscreen mode

3) MoveNext method is called again. The else block will be executed because the current machine state is 0:

awaiter = this.taskAwaiter; 
this.taskAwaiter = default(TaskAwaiter<string>); 
num = (state = -1);
Enter fullscreen mode Exit fullscreen mode

As a result, the state will be set temporarily to -1. Later the following piece of code will be run:

this.myString = awaiter.GetResult();
this.result = this.myString;
this.myString = null;
Console.WriteLine("two");
result = this.result;
Enter fullscreen mode Exit fullscreen mode

Finally, the last piece of code will be executed:

state = -2;
this.client = null;
this.result = null;
this.builder.SetResult(result);
Enter fullscreen mode Exit fullscreen mode

4) The machine is in state -2, and we received the result of our operation.

The above example shows state machine that was generated by a source code containing only one await keyword. When we use await more than once in the given method, the state machine ends up with more complicated MoveNext version. If you would like to experiment with it, I recommend https://sharplab.io/ for viewing Intermediate Language or a downloadable tool ILSpy which has similar functionality.

So far we have learned what a state machine is and how it helps us with asynchronism, but we still have one mystery left to unravel: who calls MoveNext method?

Thread pool

Thread pool is an extremely important entity that takes care of managing the threads. Thanks to it, we don't need to worry about creating a new thread or reusing existing one - thread pool is responsible for that.

One part of thread pool is a queue that stores free threads.
Image description
Since creating a new thread is costly, thread pool monitors all threads and when one of them in no longer used, it is moved to the queue. As soon as our application needs additional thread, thread pool checks if there are free threads to schedule, and only if there are no threads in the queue, a new one is created. We already know the basics of thread pool, so let's get back to our state machine and MoveNext method.

From Thread Pool's perspective, what will the execution of the following program approximately look like?

public async Task<string> DoSth()
{
     var client = new HttpClient();
     var result = await client.GetStringAsync("https://www.kamilbugno.com");
     return result;
}
Enter fullscreen mode Exit fullscreen mode

Given some simplification, the way how thread pool helps us with the execution is not very complicated:

  1. State machine will be created, and thread pool will schedule the call to the Internet by running MoveNext method (a new/different thread can be used to do it),
  2. The web request was sent, and we are waiting for response...
  3. Thread pool is notified by external entity such as a network driver that the web request completed, and it runs MoveNext method again (a new/different thread can be used to do it).
  4. We receive the result of our operation and DoSth method is completed.

By now you should know all the necessary things to consciously use the power of asynchronism in C#.

Summary

As you can see, asynchronous programming can help you to create more effective application and to use the available resources (such as CPU, memory, etc.) better. I hope this post enriched your knowledge about asynchronism and currently, you will be able to easily write asynchronous code.

You can also read this post on my official blog: https://blog.kamilbugno.com/

Top comments (2)

Collapse
 
profnachos profile image
profnachos • Edited

This tutorial was easy to follow until it got to

"2. It is possible to run async operation without await."

Up to that point, it appeared to be geared towards beginners, but then it abruptly goes into what the author acknowledges is a special case when I am still trying to get my head around await and async. As a beginner, I would like to understand common and typical uses first before venturing into more advanced and special scenarios.

Was I mistaken into thinking this was an introduction?

Collapse
 
kamilbugnokrk profile image
Kamil Bugno

Thanks for your opinion. This post focuses on explaining both basic and more complex things. For example, you don't need to know about Thread pool, state machine or ValueTask to create async code. As I wrote in the summary 'I hope this post enriched your knowledge about asynchronism' and this is the main goal of the article.