Introduction
Imagine this scenario: You’re building a WinForms application (I know, I know). When you press a button named “Get Data From Server”, you want your application to get the data from the server but you also want to be able to interact with the form while you are waiting for the data from the server. This is impossible with synchronous code (code that executes in order) since the application will freeze until we fetch the data from the server. The solution? Asynchronous code and Async/Await in .NET!
What is asynchrony?
Asynchrony allows us to write code that will run separately from our other code. In our previous example, when you press the button “Get Data From Server”, the code for fetching the data from the server will be run. While that code is running, we can run different code.
To achieve asynchrony in .NET, we will look at TPL (Task Parallel Library) and async/await.
Async/Await
To make a method run asynchronously, we mark it with the async keyword. Like in the following example:
But this isn’t enough to make this method run asynchronously. We are missing one piece, the await keyword.
Our AsyncMethod() is finally asynchronous! What does await do? When we use await (which can only be used inside an async method), we return to the caller and execute the rest of the code while SomeLongTask() is being run separately. Usually, this is run on a separate thread, but that isn’t always the case.
What will happen in this case? Well, we invoke DoWork(), then we call AsyncMethod(), which will execute SomeLongTask() on another thread, because of the await keyword, we return to DoWork() method, Console.WriteLine(“Done work”) is run, and we wait for an input from the user to exit the application. If we wait long enough, without exiting the application, SomeLongTask() will complete, Console.WriteLine(“Method has completed.”) will be run.
The await keyword can only be used inside an async method (as previously noted) and only with awaitable types (types that implement the awaiter pattern). We will take a look at the most used awaitable types called Task and Task<TResult>.
Task and Task<TResult>
The Task and Task<TResult> classes are part of the TPL (Task Parallel Library). The difference is that the former doesn’t return a result, while the latter does.
An example:
In this example, we created a method that does some work, but does not return anything. Task.Delay() returns a task that will complete after a set amount of time (in this case, the task will complete in 1000ms).
In this example, we created a method that returns a string as a result.
Async void
While we can mark an async method with Task and Task<TResult>, we can also mark it as void. There are some use cases for this (and it is mainly here to be compatible with events) but, in most cases, it is better to mark a method to return a Task and not void. One of the reasons is exception handling. Consider the following code sample:
The issue here is, since we are not awaiting, we will execute the BadExample() method and we will exit the try/catch block. Now, if an exception is thrown, it will not be caught as we have already exited the try/catch block leaving us with an unhandled exception.
By using a Task, we can rectify this:
Now, our exception will be caught.
Awaiting multiple tasks
If we want to run multiple tasks in parallel and wait for them to finish, we can use Task.WhenAll():
Task.WhenAll() creates a new task that will complete when all the tasks complete. Also, you can do something I like to call task forwarding, which we’ll look into next.
Task forwarding
Forwarding a task just means that we are returning a task, instead of awaiting it. Look at the following example:
This will work, but it will also create overhead by creating two state machines. One way to fix this is to forward (return) a task and then await it. The following example shows how to do it:
Since Task.Delay() returns a task, we just return that task in the method that is awaited. This removes the overhead that the async/await creates (avoids creating more statemachines than necessary). You can check the generated code here:
- With the async Task Task1(): https://sharplab.io/#v2:D4AQDABCCMCsDcBYAUCkBmKAmCBhCA3ihCV
- With Task Task1(): https://sharplab.io/#v2:D4AQDABCCMCsDcBYAUCkBmKAmCBhCA3ihCVJiABxQA
ValueTask
When an async method will run synchronously most of the time, it is a good choice to use ValueTask. For more information, you can look at this blog post that explains ValueTask in more detail: https://pontistechnology.com/a-quick-guide-through-tasks-and-valuetasks-in-c/
Task.Run()
To start up a task, we can use Task.Run(). This next example starts up a task and awaits its completion:
TL; DR
What is Async/Await in .NET? Asynchrony allows us to write code that will run separately from our other code. To make a method run asynchronously, we mark it with the async and await keywords. The difference between Task and Task<TResult> is that the former doesn’t return a result, while the latter does. We can mark an async method with Task and Task<TResult>, we can also mark it as void, but that should generally be avoided. To wait for multiple tasks to complete, use Task.WhenAll(). We can use task forwarding to remove the overhead of creating a state machine where it is not needed. If an async method will run synchronously most of the time, use ValueTask. To start up a new task, we can use Task.Run().