In this article, we will learn about the details of asynchronous programming with code examples i.e. what is async programming, how is it different from sync programming, how to implement it in .NET Core C# using async & await, when and where it should be used along with where it is best avoided.
These days considering the nature of applications, their load and constant demand from clients to improve the scalability of the application it becomes very important to understand & properly implement asynchronous programming concepts in your application.
If you are new to async programming then this is the right article for you to learn in detail about asynchronous programming with .NET Core C#. Like many other programming languages, C# also supports async programming. When it comes to async programming in .NET Core C# there are 2 very important keywords i.e. async & await we will learn in detail about async and await in this article.
This article assumes that you are basic knowledge of C# & .NET Core & how to build basic applications in .NET Core. For demonstrations in this article, we will be using Visual Studio Community 2022 RC with .NET 6.0.
Table of Contents
Asynchronous v/s Synchronous
Let’s try and understand the difference between sync and async processes by looking at a real-world example that is happening in our daily life. Consider a situation when there are two different guests who have walked into the restaurant and occupied 2 different tables. It is non-busy hours for the restaurant so there is only one attendant at that time who is serving guests in the restaurant. Let’s take a look at the process of serving the guests in the restaurant.
Synchronous
In synchronous mode tasks are executed one at a time and wait until the current task has been completed and then move to the execution of the next task.
- Attendant goes to attend to the guests on the 1st table
- Takes orders from the guest
- Goes to kitchen to place order for guest on the 1st table
- Waits in the kitcten till the food is ready
- Serves the food when it is ready
- Waits till guest finishes their food
- Serves the bill to guests
- Collects payment from guests
- Then goes to attend the guests of the 2nd table.
You can see from the above steps that in the case of the synchronous process next activity is blocked till the current activity or step completes. Though there are activities where there is no need to wait/block (food getting ready for 1st guest has no involvement of the attendant but still the attendant is waiting) or there are also independent activities that can happen in parallel (attendant can take orders from the guest at table 2 till the food is being prepared for guest at table 1). This wait and blocking are delaying the activity so it will take more time for the completion of the total process.
Asynchronous
In asynchronous mode tasks can complete in the background and notifies when it is completed so that you can move to the next task before the previous task completes.
- Attendant goes to attend to the guests on the 1st table
- Takes orders from the guest on the 1st table
- Goes to kitchen to place order for guest on the 1st table
- Attendant goes to attend to the guests on the 2nd table
- Takes orders from the guest on the 2nd table
- Goes to kitchen to place order for guest on the 2nd table
- Attendant waits for food to get ready and will serve to the guests whose food is ready first (1st table or 2nd table)
- Attendant waits to serve bill to guest that finish eating food first
- Attendant waits to collect payment from guest that is ready with the payment first.
In the above asynchronous process, activities are happening in parallel without any wait & blocking so there won’t be any delay in carrying out the activities or process. The attendant is always waiting to complete the task that can be completed.
In our programming world, these attendants are like threads and activities are like our lines of code. There can be long-running tasks in programming like reading a file, HTTP call to an external third party API, making database calls over the network, some heavy computations on the data, etc.
Now if we run these long-running tasks in synchronous mode then our application gets blocked and cannot perform any other task. Also, in web applications number of threads are limited and if all our available threads get blocked or busy on these long-running tasks then it won’t be able to execute new requests and any new request will have to wait in the queue.
In the above example, we are trying to run processes for guests 1 and 2 in parallel but async is not only limited to that, async is even about trying to build parallelism within the process for each guest. For example here, if a guest has ordered multiple food items then we can prepare the food items in parallel instead of preparing them sequentially.
It won’t always be possible to run multiple tasks in parallel as there will be cases like where Task 2 is dependent on the result of Task 1 then in that case these tasks cannot run in parallel.
Introduction to Asynchronous Programming
Asynchronous allows you to run the task in the background such that the process or thread is not busy or blocked. The async programming also allows us to run two or more independent tasks in parallel. This non-blocking mode and parallelization allow to avoid or reduce the delays due to waits/blocking that happens while execution of the code.
Asynchronous programming allows us to design the sequential code into logical blocks that can run together in parallel mode. Parallelization also applies to web applications on IIS where we can use threads to run multiple requests at the same time and using async for long-running tasks we can improve the scalability of the applications.
Not all tasks can run in parallel as some tasks may be dependent on the outcome of another task i.e. there can be a scenario like that we need to execute task 2 only if task 1 succeeds in that case we will have to wait for task 1 to complete and then based on the outcome of task 1 we need to decide whether we can run task 2 or not.
Asynchronous programming is not only about improving the performance or scalability of your application it is also about improving the overall user experience with the application.
In .NET Core C# we make use of async and await keywords to implement asynchronous programming. For any method to be asynchronous we have to add the async keyword in the method definition before the return type of the method. Also, it is general practice to add Async to the name of the method if that method is asynchronous.
public async Task SaveDataAsync() { //Save Data }
Async Return Types
void – The void return type can be used in asynchronous event handlers that require a void return type. For async methods that don’t return a value used Task instead of the void as async methods that return void cannot be awaited. In this case, the caller will be fire and forget method. The caller of an async method returning void cannot catch the exceptions thrown from the method.
public async void SaveDataAsync() { //Save Data }
Task – Task is used when async methods don’t contain a return statement or that contains a return statement that does not return an operand. If this method had been synchronous then it would have returned void. The use of task return type for an async method allows the caller to await the response from the async method so that caller’s completion can be suspended till the async method has finished.
public async Task SaveDataAsync() { //Save Data }
Task<TResult> – Task<TResult> is used when async methods contain a return statement that does return an operand. The use of Task<TResult> return type for an async method allows the caller to await the response from the async method so that caller’s completion can be suspended till the async method has finished.
public async Task<int> SaveDataAsync() { //Save Data return 1; }
ValueTask<TResult> – After the release of C# 7.0 it was possible for async methods to return any type that has an accessible GetAwaiter method that returns an instance of an awaiter type. In addition, the type returned from the GetAwaiter method must have the System.Runtime.CompilerServices.AsyncMethodBuilderAttribute attribute. The Task & Task<TResult> are reference types so memory allocations particularly in tight loops can impact performance so introduction on generalized async return type (starting with C# 7.0) enabled performance improvements.
public async ValueTask<int> SaveDataAsync() { //Save Data return 1; }
IAsyncEnumerable<T> – Starting with C# 8.0, an async method may return an async stream, represented by IAsyncEnumerable<T>. An async stream provides a way to enumerate items read from a stream when elements are generated in chunks with repeated asynchronous calls.
For further details on async return types, you can refer here
Benefits of Asynchronous Programming
UI is not blocked – By running long-running tasks in the background we are able to keep UI in the non-blocking mode so that users are able to interact with the applications and can perform some other operation.
Performance Improvement – We are able to improve the performance of our application by running independent tasks in parallel and if 2 or more tasks run in parallel then it will take less time for the execution of the activity.
Improves Vertical Scalability – In web applications where we have a limited number of threads in a pool for handling multiple requests, we can not block the thread by running tasks in asynchronous mode i.e. instead of waiting for a long-running task to complete we await its completion.
Drawbacks of Asynchronous Programming
Though there are clear benefits from asynchronous programming it adds to the complexity while application development. This makes asynchronous applications much more complex making it difficult to enhance or modify application functionality. It is also difficult to debug & find bugs in asynchronous applications. Since there are multiple tasks running in parallel it increases the number of objects in memory and also in some cases these objects have to be longer in memory till all the tasks are completed.
When to Apply Async Programming
Async Programming can be used for the long-running blocking task. Whenever there is a blocking piece of code that can run independently of the rest of the process then in that case we can run that code as an async task. This async task mode will run that piece of code on a different thread instead of the main thread and the main thread can be free to keep the application in the responding state.
Examples for long-running tasks in programming can be like reading or writing a file, HTTP calls to an external third party API, making database calls over the network, some heavy computations on the data, writing application logs to a file etc.
Implementation of Async Programming in C# .NET Core
Let’s learn in detail about how to implement asynchronous programming in C# .NET Core
Create new Project for demonstration
Using Visual Studio 2022 with .NET 6 create a new project of type Console App as per the screens shown below.
- Launch Visual Studio 2022 and select Create a new Project
- Select project type as Console App and click on Next
- Type name of the project as ProCodeGuide.Sample.Async and click on Next
- Select framework as .NET 6.0 and click on Next
The above steps will create a new project and load it in Visual Studio 2022.
Modify Program.cs file
Add below code to Program.cs file in console App Project
public class Program { static readonly Stopwatch timer = new Stopwatch(); public static void Main(string[] args) { timer.Start(); Console.WriteLine("Program Start - " + timer.Elapsed.ToString()); Task1(); Task2(); Console.WriteLine("Program End - " + timer.Elapsed.ToString()); timer.Stop(); } private static async Task Task1Async() { Console.WriteLine("Async Task 1 Started - " + timer.Elapsed.TotalSeconds.ToString()); await Task.Delay(2000); Console.WriteLine("Async Task 1 Completed - " + timer.Elapsed.TotalSeconds.ToString()); } private static async Task Task2Async() { Console.WriteLine("Async Task 2 Started - " + timer.Elapsed.TotalSeconds.ToString()); await Task.Delay(3000); Console.WriteLine("Async Task 2 Completed - " + timer.Elapsed.TotalSeconds.ToString()); } private static void Task1() { Console.WriteLine("Task 1 Started - " + timer.Elapsed.TotalSeconds.ToString()); Thread.Sleep(2000); Console.WriteLine("Task 1 Completed - " + timer.Elapsed.TotalSeconds.ToString()); } private static void Task2() { Console.WriteLine("Task 2 Started - " + timer.Elapsed.TotalSeconds.ToString()); Thread.Sleep(3000); Console.WriteLine("Task 2 Completed - " + timer.Elapsed.TotalSeconds.ToString()); } }
In the above code, we have added methods Task1 & Task2 and for both methods, we have added sync & async version of the methods. These task methods are dummy task methods that simulate a task by adding delay to the execution. In real applications, these will be codes for file operations or database operation or computation tasks.
In these task methods (Task1 & Tasks) we are also printing the method start time and as well method completion time so that we have a fair idea of how execution is happening of these methods.
In the main method, we will be calling these task methods (Task1 & Task2) one after the other. First, we will call the sync version of task methods & check the output
Also in the main method, we have added code to print the program start time and End time so that we know when these methods were called after the application started.
For logging time in the program Main method and also in Task methods we have made use of the stopwatch class to start the timer and then printed elapsed time from the timer at each event.
We have made use of the following keywords to make methods asynchronous
async – When we add this keyword in the function definition (before function name) then it allows us to call that function asynchronously i.e. async keyword is used to qualify a function as an asynchronous function
await – When we want to call an async function asynchronously then we use this await keyword. This keyword is added below the function call.
Let us first check the output of the above code with the sync version of Task methods. After running the above code we can see the below output in the console window.
The above results are as expected i.e. Task1 started as soon as the program started and took about 2 secs to complete as it has a delay of 2 seconds. Immediately after Task1 was completed Task2 started executing and took about 3 seconds to complete as it has a delay of 3 seconds. So total program execution took approximately 5 seconds to complete.
Modify Main method to await instead of blocking
We saw the output of the code above with the sync version of Task methods which is a bad practice of blocking as this code blocks the thread executing it from doing any other work.
Now let’s modify the Main method in the Program.cs to call the async version of Task methods (Task1Async & Task2Async) so that threads don’t block while the tasks are running. We will make use of await keyword with an async version of task methods that provides a non-blocking way to start a task.
Modify the code in the Main method in Program.cs file as shown below.
public static async Task Main(string[] args) { timer.Start(); Console.WriteLine("Program Start - " + timer.Elapsed.ToString()); await Task1Async(); await Task2Async(); Console.WriteLine("Program End - " + timer.Elapsed.ToString()); timer.Stop(); }
We have modified the Main method to make use of an async version of task methods (Task1Async & Task2Async). To call these methods asynchronously we are making use of await keywords before calling the functions as shown in the above code.
After running the above code we can see the below output in the console window.
The above results are similar to the sync version i.e. total program execution took approximately 5 seconds to complete. i.e. still both methods Task1Async & Task2Async were executed sequentially. This is because as code is yet to take advantage of the asynchronous programming in C# .NET Core.
The above code doesn’t block while performing Task1 but it will not start Task2 till Task1 completes. If you are not able to call Task1 & Task2 in parallel due to dependency then this is the only change required to run the application in non-blocking mode. A GUI application will not go into a not responding state by only this change.
However, in this sample program, we can execute Task1 and Task2 in parallel as there is no dependency between them i.e. you don’t want each Task method to be executed sequentially. So let’s make the code change to start Task2 before awaiting the completion of Task1.
Modify Main method to start Task1 & Task2 concurrently
In real applications, if you have tasks that can run independently then you want to start those tasks immediately so that they can execute in parallel. Running tasks in parallel will get everything done in a lesser time compared to running them sequentially.
For running tasks in parallel you need to start the Task and hold on to the Task object (System.Threading.Tasks.Task) that represents the work. You will have to await each task object before working towards the program completion.
Let’s make these changes to the Main method in Program.cs as shown below
public static async Task Main(string[] args) { timer.Start(); Console.WriteLine("Program Start - " + timer.Elapsed.ToString()); Task task1Task = Task1Async(); Task task2Task = Task2Async(); await task1Task; await task2Task; Console.WriteLine("Program End - " + timer.Elapsed.ToString()); timer.Stop(); }
In the above code, we have stored the task (task1Task & task2Task) for operations (Task1Async & Task2Async) when they start. We have not awaited the task as they start. Next, we have added code to await these task (task1Task & task2Task) objects after starting both Task and before completing the Main method.
After running the above code we can see the below output in the console window.
Now we see from the above output that we have for performance benefit from asynchronous programming i.e. our total program has been completed in approximately 3 seconds instead of 5 seconds which was taken earlier. We can see that both task1 & task2 started almost at the same time when the program started. Both Task1 & Task2 were executed in parallel so there was an improvement in the code execution time.
Modify Main method to await Task efficiently
In the above code, we are awaiting multiple Tasks so in code there is more than 1 statement for await. Now if we have many tasks then there will be a series of await statements. This code can be improved by making use of methods available in the Task class.
Let’s make these changes to the Main method in Program.cs as shown below
public static async Task Main(string[] args) { timer.Start(); Console.WriteLine("Program Start - " + timer.Elapsed.ToString()); Task task1Task = Task1Async(); Task task2Task = Task2Async(); await Task.WhenAll(task1Task, task2Task); Console.WriteLine("Program End - " + timer.Elapsed.ToString()); timer.Stop(); }
In the above code, we have modified it to make use of the method WhenAll from System.Threading.Tasks.Task class that returns a Task that completes when all the tasks in its argument list have been completed
Another option available is to use WhenAny
the method from System.Threading.Tasks.Task, which returns a Task<Task> that completes when any of its arguments are complete. You can use this WhenAny
when you have a scenario like you want to wait only till any of the tasks completes.
After running the above code we can see the below output in the console window.
The above results are similar to the async version i.e. total program execution took approximately 3 seconds to complete. There is no change in code execution but we have simplified the code using the methods available in the Task class.
So far we have covered how to use asynchronous programming techniques to improve code performance in C# .NET Core. Next, let’s look at how to handle exceptions thrown by async methods
Handling Excpetions from async methods
Asynchronous functions also throw exceptions in the same way as synchronous functions. Task throws exceptions when that cannot complete successfully due to some exception. For exceptions handling, you need to write code in the same way as that reads like a series of synchronous statements. The calling code can handle/catch those exceptions when the task is awaited.
For demonstration, let’s assume that Task1Async throws some exception such that it is not able to complete successfully. We can simulate this exception by modifying code in Task1Async method to throw some exceptions as shown below.
private static async Task Task1Async() { Console.WriteLine("Async Task 1 Started - " + timer.Elapsed.TotalSeconds.ToString()); await Task.Delay(2000); throw new Exception("Async Task 1 Failed"); }
Next, we need to handle/catch this exception in the Main method where we are awaiting the call to this async Task1Async method. Modify the Main method in Program.cs as shown below.
public static async Task Main(string[] args) { timer.Start(); Console.WriteLine("Program Start - " + timer.Elapsed.ToString()); Task task1Task = Task1Async(); Task task2Task = Task2Async(); try { await Task.WhenAll(task1Task, task2Task); } catch (Exception ex) { Console.WriteLine(ex.Message.ToString()); } Console.WriteLine("Program End - " + timer.Elapsed.ToString()); timer.Stop(); }
After running the above code we can see the below output in the console window.
We can see from the above output that Task1Async has thrown an exception it was handled by the catch block of the calling code and accordingly exception message has been printed to the console. We can see that task whenAll waiting for all tasks to be complete and then handled the exception from Task1Async method.
Summary
We learnt about what is asynchronous programming and how to make use of async & await keywords in C# .NET Core to implement asynchronous programming techniques. Also, we saw how asynchronous programming helps us to improve the performance of code by calling methods in parallel and thus reducing the overall execution time.
In my next post, we will make use of these asynchronous programming techniques and learn how to design async Web API in ASP.NET Core
Please provide your suggestions & questions in the comments section below
Meanwhile, you can check my other trending articles on Build Resilient Microservices (Web API) using Polly in ASP.NET Core & Microservices with ASP.NET Core 3.1 – Ultimate Detailed Guide
Download Source Code
Here you can download the complete source code for this article demonstrating how to implement asynchronous programming in C# .NET Core
https://github.com/procodeguide/ProCodeGuide.Sample.Async
Awesome content, very informative. Nicely done.
Thanks for your feedback!
Very clear and understandable examples. Thanks Bro.
Thanks for your feedback!