Before digging deeper into .net Asynchronous, let’s take a brief look at its history.
In the early 2000s, the .NET Framework 1.0 introduced the IAsyncResult pattern, known as the Asynchronous Programming Model (APM), or the Begin/End pattern. Later, in .NET Framework 2.0 the Event-based Asynchronous Pattern (EAP) was added. Starting from .NET Framework 4, TAP or Task-based Asynchronous Pattern superseded both EAP and APM, and also provided the ability to build migration routines more easily than using earlier patterns.
In this article, we are going to talk about the Task-based Asynchronous Pattern (TAP), focusing on its fundamentals.\
What does Asynchronous mean?
One commonly misunderstood aspect of asynchronous programming in .NET is that it will improve the "performance". This is often taken to mean that "it will make my code run faster", but this is totally misleading. Asynchronous programming will not make your code execute any faster.
What asynchronous programming actually does is increase the number of requests that can be handled at the same time with the same resources. The same amount of threads can handle many more simultaneous requests in an asynchronous system than in a synchronous one. In short, Asynchronous programming doesn't improve performance - it improves throughput by minimising idle time.
Let’s demonstrate this by using the analogy of preparing breakfast. To prepare breakfast, there may be a number of steps such as:
- Pouring a cup of coffee.
- Heating up a pot, then boiling two eggs.
- Heating up a pan, then frying three slices of sausage.
- Toasting two pieces of bread.
- Adding butter and jam to the toast.
Using a synchronous method, we start by heating up a pot with some water in it, then crack two eggs and cook them. In the meantime, you will stare intently at it, and wait patiently while the items are cooking. You would do nothing else during that time.
Next, after the eggs are ready, you would continue by heating up a pan, then fry three slices of sausage. Again, you will wait patiently while they are cooking and do nothing else. This will be repeated for the other tasks. I am sure you get the point.
Using an asynchronous method, on the other hand, while you are cooking the eggs, you can also heat up a pan to fry the sausage. In this way, you minimise the idle time by not having to wait for something to finish. You can prepare your breakfast much faster even though you don't actually work any faster than you normally do.
Let’s get geek
In this part, let’s see some examples of Synchronous and Asynchronous code. Firstly, Synchronous code:
As you see in the example code above, computers will interpret this code on a per-statement basis. They will block on each statement until the work is complete before moving on to the next statement, and the next task won't be started until the earlier task has completed. It would take much longer to create breakfast.
In Asynchronous code, as you can see in the image below, the computer can process the next task without having to wait for the previous one to be completed, with the addition of keywords such as async, await as well as Task.WhenAny to await tasks efficiently. We’ll explain them in more detail later.
To start transforming the code from Synchronous into Asynchronous, we must firstly update all the methods to be Asynchronous with the addition of the keyword async Task<T>. In the sample code snippet below, you can see that async Task<egg> is used to make the method asynchronous. Later, you can call it by using the "var eggsTask = BoilEggsAsync(2);" statement.
Asynchronous code in .NET uses only three return types:
- Task: Represents work being done that will eventually return control to the caller.
- Task<T>: Represents work being done that will eventually return an object of type T to the caller.
- void: Makes the method a true fire-and-forget method.
When all methods have become asynchronous, the next step is to call each method and store in a variable like eggsTask, sausageTask, and toastTask. Next, to efficiently await each task’s completion, we could use await Task.WhenAny, which returns a Task<Task> that completes when any of its arguments complete. You can await the returned task, knowing that it has already finished. The following code shows how you could use WhenAny to await the first task to finish and then process its result. After processing the result from the completed task, you remove that completed task from the list of tasks passed to WhenAny. Another way would be to use WhenAll to efficiently await all tasks to complete.
When we compare the Asynchronous and Synchronous code, you can clearly seen that there is a big difference in time spent to prepare breakfast. The Synchronous way took about 18 seconds, while Asynchronous way was completed in just over 9 seconds.
Asynchronous When and Why
There are a number of benefits to using Asynchronous code, such as improved application performance and enhanced responsiveness.
However, like all things in programming, it’s not something that you should use in every instance. In fact, there are some situations in which you should avoid it.
Asynchronous is best suited when you are trying to process the following requests:
- I/O bound requests
For example: writing/reading to a file or database, making API calls, calling hardware like printers, etc.
- CPU bound requests (requires CPU time)
For example: looping through thousands of objects, complex or lengthy calculations, processing millions of data points, etc.
There are some reasons, why we should use asynchronous:
- Responsive UI (Client-side)
Example: Let’s say a mobile app has two buttons - say Button X and Button Y. Button X gets the detailed transaction history for the last for 30 days (let's say the request could take 10 seconds to process), while Button Y changes the theme colour of the app. When a user selects Button X, the UI must not freeze. The user must be allowed to select Button Y to change the theme colour while the app is fetching transaction data in the background. With Async, we can solve this problem.
- Fire and Forget / Process Independent Data (Server-Side)
Example: An API first returns the data requested by the user. The user is not kept waiting, while in the background process, some complex calculations are undertaken that require CPU time. In the end, it will send the results to a third-party service. In this situation, we could use an asynchronous method to make a thread for doing all of the background processes.
Of course, there are some time we should not use Asynchronous:
- When using a micro-service that has very small CPU-bound operations and is always getting requests from users. In this situation, an async method is not needed.
- When a service is calling a database that cannot be scaled up. If we make the code async, it won't help because the database will quickly become the bottleneck.
- When an application has very slow I/O bound work, but does not receive a lot of requests. Here it is not necessary to add async.
- When a monolithic application has a lot of CPU-bound computations, but each computation is dependent on the previous one.
Asynchronous programming can be a good technique to use. It can make our apps more responsive and enable our system to process more requests by minimising idle time. As long as we are clear about when and where to use it, it can be a great technique.
Romadhona Armayndo - Analyst Programmer