Table of Contents
In this post we'll see what asynchronous programming is, and most importantly, when and where to use it.
1 - What is asynchronous programming?
As the name suggests, it allows us to write code that executes in a parallel manner.
It’s very important not to confuse it with TPL
(Task parallelization), as although they are similar concepts, they are not the same.
A very common mistake when starting to code is to think that more Task
means multithreading, or that each Task
will be a new thread, which does not always have to be the case. When we invoke Task
we are calling the Task parallel
library, which will handle all the thread management for us.
The great advantage of using async / await
is that it allows us to write parallel code in a very simple and easy way.
I'll try to write this post with a real-world example, as I feel like that will make things clearer.
2 - How to use asynchronous programming in C# with Async / await
The first thing is to see how we write code to make it asynchronous. The code is available on GitHub and I’ll use the same example as in the post on how to connect to a database.
We have the following code that we’ve decided to make asynchronous:
public Articulo GetArticulo(int id)
{
using(MySqlConnection conexion = new MySqlConnection(conexionString))
{
//Codigo para el select
conexion.Open();
MySqlCommand comando = new MySqlCommand();
comando.CommandText = "select * from articulo where id = ?articuloId;";
comando.Parameters.Add("?articuloId", MySqlDbType.Int32).Value = id;
comando.Connection = conexion;
Articulo articulo = new Articulo();
using (var reader = comando.ExecuteReader())
{
while (reader.Read())
{
articulo.Id = Convert.ToInt32(reader["Id"]);
articulo.Titulo = reader["Titulo"].ToString();
articulo.Contenido = reader["Contenido"].ToString();
}
return articulo;
}
}
}
As we can see, this code queries the database. The way to convert this method to asynchronous is very easy; we just need to indicate that instead of returning an Articulo
class, it returns Task<T>
where, as we saw in generics, T
is Articulo
.
public Task<Articulo> GetArticulo(int id){...}
You will see several errors pop up, the first being that we need to use the System.Threading.Tasks
library, and the second is that we’re not actually using tasks inside our method.
If we've marked the method as Task<T>
, it means we want it to run asynchronously. In this situation, we can call the database asynchronously. Let’s see how.
The first step is opening the connection, which we can do asynchronously. Currently, we do conexion.Open();
. A common convention when writing libraries is that if a method is asynchronous, we append async
to its name, so we can open the database connection with conexion.OpenAsync();
. But this method DOES NOT actually open the connection right away; we must wait for it to open. For that, we use the await
keyword. So it looks like this:
await conexion.OpenAsync();
When we use await
, the method’s header must include the async
keyword, so the method will look like this:
public async Task<Articulo> GetArticulo(int id){...}
And now we have our asynchronous method:
public async Task<Articulo> GetArticulo(int id)
{
using(MySqlConnection conexion = new MySqlConnection(conexionString))
{
//Codigo para el select
await conexion.OpenAsync();
MySqlCommand comando = new MySqlCommand();
comando.CommandText = "select * from articulo where id = ?articuloId;";
comando.Parameters.Add("?articuloId", MySqlDbType.Int32).Value = id;
comando.Connection = conexion;
Articulo articulo = new Articulo();
using (var reader = await comando.ExecuteReaderAsync())
{
while (reader.Read())
{
articulo.Id = Convert.ToInt32(reader["Id"]);
articulo.Titulo = reader["Titulo"].ToString();
articulo.Contenido = reader["Contenido"].ToString();
}
return articulo;
}
}
}
But making the method asynchronous is not enough; when we use Task<>
somewhere in the process, we need to run the whole process asynchronously. That means all the methods in the process should use Task<T>
.
For example, the method that calls our recently updated method. We’ll convert it from:
public Articulo ConsultarArticulo(int id)
{
return _articuloRepository.GetArticulo(id);
}
to:
public async Task<Articulo> ConsultarArticulo(int id)
{
return await _articuloRepository.GetArticulo(id);
}
And like this, all the methods used by our asynchronous process.
2.1 - Await in C#
As I just mentioned, to get the object we want from a method that returns Task<T>
, we need to wait for it, and for that, we use the await
keyword.
But we must be careful, as we can easily turn our asynchronous code into synchronous code. For example, the following code:
Articulo articulo1 = await _articuloRepository.GetArticulo(1);
Articulo articulo2 = await _articuloRepository.GetArticulo(2);
Articulo articulo3 = await _articuloRepository.GetArticulo(3);
Executes the statements one at a time, in order; it waits for the first with id 1 to finish, then starts the second, and so on.
To execute these actions asynchronously, we need to call the method that returns a Task
, store that Task
in a variable, and await them afterward:
Task<Articulo> taskArticulo1 = _articuloRepository.GetArticulo(1);
Task<Articulo> taskArticulo2 = _articuloRepository.GetArticulo(2);
Task<Articulo> taskArticulo3 = _articuloRepository.GetArticulo(3);
Articulo articulo3 = await taskArticulo3;
Articulo articulo2 = await taskArticulo2;
Articulo articulo1 = await taskArticulo1;
2.2 - Task.WhenAll Method in C#
Alternatively, when we have multiple tasks to execute, C# provides us with a method called Task.WhenAll(IEnumerable<Task>)
that lets us specify a list of Task
to run.
Task<Articulo> taskArticulo1 = _articuloRepository.GetArticulo(1);
Task<Articulo> taskArticulo2 = _articuloRepository.GetArticulo(2);
Task<Autor> taskAutor1 = _autorRepository.GetAutor(1);
await Task.WhenAll(taskArticulo1, taskArticulo2, taskAutor1);
Articulo articulo1 = taskArticulo1.Result;
Articulo articulo2 = taskArticulo2.Result;
Autor autor1 = taskAutor1.Result;
As we can see, we can include any type of Task
in our list, and later access the result via the .Result
property.
Note: If we try to access the .Result
property before awaiting with await
, we can't, as the task won’t have completed yet.
We should prioritize using Task.WhenAll
over the multiple-await pattern for several reasons:
- The code looks cleaner.
- It propagates errors correctly. If you have 10 tasks with
await
and one of the first fails, you could lose the error. - Using
WhenAll
waits until ALL the tasks finish, even if there are errors. If you handle things manually withtry{}catch()
and one fails, part of your code may want to throw an exception while another part is still waiting , this can lead to bugs or hangs.
3 - When to use asynchronous programming in C#
We should use asynchronous programming whenever possible, plain and simple. The benefits it brings , especially in performance and responsiveness , are enormous.
Here’s a clear example: imagine calling 3 APIs from your program, one after another synchronously. The timing chart looks like this:
As we can see, the calls to all 3 external services take a total of 15.5 seconds.
Whereas if we make these calls asynchronously, it takes only 6 seconds.
This logic applies to any service. For example, if you're querying several tables from the database, you can make them all asynchronously.
Conclusion
Making all calls asynchronously makes our code much faster, because when making multiple calls to external services, for example, we don’t have to wait one by one for each to return. Instead, we wait until all are done and ready for us.
Using async / await
makes our code much clearer and cleaner than using threads, which, honestly, is a mess.
If there is any problem you can add a comment bellow or contact me in the website's contact form