The asynchronous programming allows suspending tasks without blocking the thread while they wait.
This is very useful when the program spends a lot of time waiting for external things: network, disk, databases, timers, APIs… Instead of staring into infinity, Rust can use that time to advance with other tasks.
The problem of waiting
Suppose we make two HTTP requests and each takes one second.
If we execute them sequentially, the program waits for the first one and then waits for the second one. Total: two seconds.
let a = pedir_a().await;
let b = pedir_b().await;With async we can organize the work so that multiple operations advance simultaneously, as long as they are waiting for I/O and not consuming CPU.
Async does not make a sum faster. If your task is to calculate numbers non-stop, you need real parallelism. Async shines when there are waits.
What async does
When we mark a function as async, the function does not directly return the result. It returns a Future.
async fn saludar() -> String {
String::from("Hola")
}Although it seems to return String, it actually returns something that represents a result that will be available later. That something is a Future.
What .await does
To get the result of a Future, we use .await.
async fn saludar() -> String {
String::from("Hola")
}
#[tokio::main]
async fn main() {
let mensaje = saludar().await;
println!("{}", mensaje);
}When the future returns Pending, .await suspends that task and allows the runtime to execute others. This does not make a blocking operation asynchronous: a slow synchronous call within the future would still block the thread.
We need a runtime
Rust defines Future, async, and .await, but it does not come with a complete asynchronous runtime in the standard library.
To execute async code we need an executor, typically provided by a runtime like Tokio or async-std.
Tokio is one of the most widely used options:
cargo add tokio --features fullAnd we can use #[tokio::main] to start the runtime:
use tokio::time::{sleep, Duration};
async fn tarea_lenta() {
sleep(Duration::from_secs(1)).await;
println!("Terminé");
}
#[tokio::main]
async fn main() {
tarea_lenta().await;
}Futures are lazy
An important detail: creating a Future does not execute it automatically.
async fn tarea() {
println!("Ejecutando tarea");
}
#[tokio::main]
async fn main() {
let futuro = tarea();
// Nothing has been printed here yet
futuro.await;
}The future starts advancing when someone awaits it with .await or when it is handed over to the runtime with tools like tokio::spawn.
Async is not automatic parallelism
This is important so we don’t get a nasty surprise.
tarea_1().await;
tarea_2().await;This executes tarea_1, waits for it to finish, and then executes tarea_2. It is asynchronous, yes, but it is still sequential.
To execute multiple tasks concurrently we will use tokio::join!, tokio::spawn, or tokio::select!. We’ll see that in the next post.