Introduction to asynchronous programming in .Net
Have you ever heard about asynchronous programming but didn’t know what it meant? Or perhaps you did some research and found out, but weren’t sure when to apply it? This blog post aims to demystify asynchronous programming, illustrating its advantages and when it's most effective, especially for those collaborating with a technology consulting firm.
What is asynchronous programming?
By definition “asynchronous” means “Having many actions occurring at a time, in any order, without waiting for each other.”
The typical way of writing code is synchronously, meaning that each part of the code is executed as a result of some other part. This means that your program will wait for a certain process to finish executing (e.g. network call or I/O operation) - before continuing and thus, blocking the entire program for an unknown amount of time.
Writing code asynchronously will result in returning control back to the caller whenever a more time-consuming task occurs.
This means that, in simple words, whoever called your asynchronous method (e.g. another method) will be able to continue doing other work without waiting for the response. This will increase responsiveness of any User Interface the program might have and allow for better scalability.
Here is a graph that illustrates the difference between synchronous and asynchronous execution:
Or in other, simpler words:
Synchronous means ” Wait for me to finish before doing something else”.
Asynchronous means “It’s ok, keep going while I finish’”.
Why use asynchronous programming
Writing asynchronous code doesn’t necessarily improve performance in terms of the time it takes for your code to be executed. Rather, it improves the throughput capabilities of your system, making it able to handle more requests without failing.
By utilizing the abilities of your server to handle requests, you make your system more responsive and easier to scale.
By definition “scalability” means “The capability of a system, network, or process to handle a growing amount of work, or its potential to be enlarged to accommodate that growth”.
There are a couple of methods to scale a server, so that it is able to handle more requests.
- Horizontal scaling
- Vertical scaling
With horizontal scaling, when the load becomes too great for a single server to handle, additional servers are added. Requests are distributed among these servers and thus, resolving the problem. Other problems might appear if the system is using a non-distributed database or cache component.
With vertical scaling when the load becomes too great for the server, its resources are increased, e.g. memory, storage, CPU power, allowing it to handle more requests.
When to use it
As mentioned before, asynchronous implementation might not improve the code performance that much. This means that in order to gain the most benefits we must know when to apply it.
We have two types of time-consuming tasks that our code performs:
- Computational bound tasks
- I/O bound tasks
Computational bound tasks use the computation speed of the machine to run quickly. This means that the faster processor you have, the faster those tasks will be completed.
Computational bound tasks do not wait for or need any other input while occupying the time of the processor. Such tasks are expensive business algorithms or complex mathematical equations. They do not benefit from asynchronous programming and on some occasions using async code may slow down the whole process.
I/O bound tasks usually wait for the response of an external source before continuing with the execution. These external sources can be databases, APIs, services and others.
When making calls to external sources, the processor must wait for the response making I/O bound tasks the perfect candidate for an asynchronous approach. While the system is waiting, a “marker” is left, which the code returns to later. Meanwhile, control is given back to the caller of the method, possibly all the way up to the User Interface. This ensures that the user will not meet a blocked or non-responsive system while waiting for his request.
Syntax
As the title implies, we will be using C# and some of the latest features of .Net in the examples below.
Async/ Await Keywords
Async The first thing you must do to implement a method asynchronously is mark it with the async modifier.
This will do two things:
- It will allow for the await keyword to be used in this method once or several times
- It will transform the method into a state machine when complied
Just marking the method with the async modifier does not make it run asynchronously. Without using await inside the body of the method, it will run synchronously like any other method.
Async and contracts (interfaces)
Marking a method with the async modifier is an implementational detail. This means that you don’t specify whether the method is async in the contract (interface), which is helpful when, for example, you want to mock a certain method to return a particular value, otherwise received from an external source.
Async Return Types
Async methods can have the following return types:
- From C# 5.x forward:
- Void
- Task
- Task
- From C# 7.x forward:
- Any type with accessible GetAwaiter method
- From C# 8.x forward:
- IAsyncEnumerable
Let’s briefly explain each of the above:
- Avoid using void when writing asynchronous code, whenever you can. It’s only appropriate for event handlers in certain scenarios.
- The Task class represents a single operation, that does not return a value and is usually executed asynchronously.
- The Task class represents a single operation that is also executed asynchronously but returns a value.
- Having an accessible GetAwaiter method allows any type to be returned with an async modifier.
- IAsyncEnumerable is an async stream, enabling an async method to return collections. It is an asynchronous version of I Enumerable and can be used to iterate through asynchronous functions.
Await
The await keyword is an operator. It can only be used inside the body of a method marked with the async modifier. What it does is:
- It tells the compiler that the async method can’t continue until the awaited asynchronous process is completed
- It tells the compiler to insert a possible suspension/resumption point into a method marked as async
- It gives control back to the caller of the async method (potentially freeing up the thread)
When you write “await someTask” the compiler generates code, that checks whether the operation has already been completed. If it hasn’t, the generated code hooks a continuation delegate (the marker) to the awaited object. When the operation is completed, the aforementioned continuation delegate will be called, thus re-entering the method and continuing the execution.
Awaitables
As of C# 7.x and onward an “Awaitable” is any type that exposes a GetAwaiter method, which returns a valid “Awaiter”. This means that such types can be used with the await keyword in an asynchronous method.
Awaiters
An “Awaiter” is any type returned from a GetAwaiter method that implements the System.Runtime.CompilerServices.INotifyCompletion interface and on occasions the System.Runtime.CompilerServices.ICriticalNotifyCompletion interface.
Best practices
Avoid using "void" as a return type for your async methods
Always avoid using void as a return type when using an asynchronous method as it crashes the process in case of failure. Using “Task” will assure that a proper exception, which you can handle, is thrown.
The right way to do the above is to use “Task.Run()” and “Task” to ensure your program won’t crash, if something goes wrong.
Avoid using Task.Result()
Applying “Taks.Result()” will result in blocking the current thread, while it waits for the result of your task, making the use of asynchronous programming meaningless.
Instead, you should “await” to get the required result.
Using “Task.Result()” is a way to implement Sync over Async but this is rarely the right way to go. If possible, you should strive to use a truly synchronous API, should synchronicity be required.
Use the .Net Core 3.x “IAsyncEnumerable” for collections
The new, in .Net Core, IAsyncEnumerable allows for data to be sent back as soon as it’s processed.
Summary
Asynchronous programming continues to be a critical skill for developers in 2024, adapting to the latest technologies and frameworks. It allows you to do more while keeping the simplicity and readability of your code. Not only is it easy to implement and manage, but it also increases responsiveness, helps vertical scalability, and happens to be the best for long IO tasks with its simple syntax that gets regularly improved with C# versions. This evolving approach underscores its increasing importance in creating efficient, user-friendly applications, making it a key consideration for any software development firm or IT services consultancy aiming to stay at the forefront of technology.
What else do you need to give async a try? If you have any questions or suggestions on improving this content, feel free to leave them in the comment section below.
Also, if you enjoyed reading this post, please share it with your colleagues and friends. Happy coding!