Async/Await
La programmazione asincrona in Rust permette di scrivere codice concorrente efficiente senza creare un thread per ogni operazione. Grazie al sistema async/await, le operazioni di I/O possono essere sospese e riprese senza bloccare il thread corrente.
Funzioni Async e .await
Una funzione async restituisce un Future che rappresenta un valore che sarĂ disponibile in futuro:
async fn saluta(nome: &str) -> String {
format!("Ciao, {}!", nome)
}
async fn esegui() {
let messaggio = saluta("Mondo").await;
println!("{}", messaggio);
}
Il .await sospende l’esecuzione della funzione corrente fino a quando il Future non è pronto. Importante: .await può essere usato solo all’interno di funzioni o blocchi async.
Il Trait Future
Ogni funzione async restituisce un tipo che implementa il trait Future:
use std::future::Future;
// Queste due dichiarazioni sono equivalenti
async fn calcola() -> i32 {
42
}
fn calcola_equivalente() -> impl Future<Output = i32> {
async { 42 }
}
Un Future non fa nulla finché non viene “guidato” (polled) da un runtime. Questo è il concetto di lazy evaluation dei Future in Rust.
Blocchi Async
I blocchi async creano Future anonimi inline:
async fn esempio() {
let futuro = async {
// operazioni asincrone qui
let x = 10;
let y = 20;
x + y
};
let risultato = futuro.await;
println!("Risultato: {}", risultato);
}
I blocchi async possono catturare variabili dall’ambiente circostante, come le closures:
async fn esempio_cattura() {
let nome = String::from("Rust");
let futuro = async move {
println!("Linguaggio: {}", nome);
};
futuro.await;
}
Runtime: Tokio
Rust non include un runtime asincrono nella libreria standard. Il runtime piu utilizzato è Tokio. Per usarlo, aggiungi nel Cargo.toml:
[dependencies]
tokio = { version = "1", features = ["full"] }
Ecco un esempio completo con Tokio:
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
println!("Inizio");
let task1 = async {
sleep(Duration::from_secs(2)).await;
println!("Task 1 completato");
};
let task2 = async {
sleep(Duration::from_secs(1)).await;
println!("Task 2 completato");
};
// Esegue entrambi i task concorrentemente
tokio::join!(task1, task2);
println!("Fine");
}
Spawning di Task Asincroni
tokio::spawn crea un nuovo task asincrono che viene eseguito in modo concorrente:
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
let handle = tokio::spawn(async {
sleep(Duration::from_millis(500)).await;
"Risultato dal task"
});
// Altre operazioni mentre il task è in esecuzione
println!("Task lanciato, attendo...");
let risultato = handle.await.unwrap();
println!("{}", risultato);
}
Combinare Future con join! e select!
join! esegue piĂą Future concorrentemente e attende che tutti completino:
use tokio::time::{sleep, Duration};
async fn scarica_pagina(url: &str) -> String {
sleep(Duration::from_millis(100)).await;
format!("Contenuto di {}", url)
}
#[tokio::main]
async fn main() {
let (pag1, pag2, pag3) = tokio::join!(
scarica_pagina("https://esempio.it/1"),
scarica_pagina("https://esempio.it/2"),
scarica_pagina("https://esempio.it/3"),
);
println!("{}\n{}\n{}", pag1, pag2, pag3);
}
select! attende il primo Future che completa e cancella gli altri:
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
tokio::select! {
_ = sleep(Duration::from_secs(1)) => {
println!("Timer scaduto");
}
_ = async {
sleep(Duration::from_millis(500)).await;
} => {
println!("Operazione completata per prima");
}
}
}
Canali Asincroni
Tokio fornisce canali asincroni simili a quelli della libreria standard:
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel(32); // buffer di 32 messaggi
let tx2 = tx.clone();
tokio::spawn(async move {
tx.send("Ciao dal task 1").await.unwrap();
});
tokio::spawn(async move {
tx2.send("Ciao dal task 2").await.unwrap();
});
while let Some(msg) = rx.recv().await {
println!("Ricevuto: {}", msg);
}
}
Tokio offre anche oneshot per comunicazioni singole e broadcast per multi-consumer:
use tokio::sync::oneshot;
#[tokio::main]
async fn main() {
let (tx, rx) = oneshot::channel();
tokio::spawn(async move {
tx.send(42).unwrap();
});
let valore = rx.await.unwrap();
println!("Ricevuto: {}", valore);
}
Pinning
Alcuni Future devono essere “pinned” in memoria perché contengono auto-referenze. Pin garantisce che il dato non venga spostato:
use std::pin::Pin;
use std::future::Future;
async fn operazione() -> i32 {
42
}
fn richiede_pin(futuro: Pin<&mut dyn Future<Output = i32>>) {
// ...
}
async fn esempio_pin() {
let mut fut = Box::pin(operazione());
// fut è ora Pin<Box<dyn Future<Output = i32>>>
let risultato = fut.as_mut().await;
println!("{}", risultato);
}
In pratica, Box::pin() è il modo piu semplice per creare un Future pinned.
Runtime async-std
Un’alternativa a Tokio è async-std, che offre un’API simile alla libreria standard:
[dependencies]
async-std = "1"
use async_std::task;
use std::time::Duration;
fn main() {
task::block_on(async {
let handle = task::spawn(async {
task::sleep(Duration::from_millis(100)).await;
"Completato"
});
let risultato = handle.await;
println!("{}", risultato);
});
}
Conclusione
La programmazione asincrona in Rust offre concorrenza efficiente senza il costo dei thread del sistema operativo. Le funzioni async e l’operatore .await rendono il codice asincrono leggibile quasi quanto quello sincrono. Ricorda che i Future in Rust sono lazy e necessitano di un runtime (come Tokio o async-std) per essere eseguiti. Usa join! per attendere piu Future insieme, select! per reagire al primo che completa, e i canali asincroni per la comunicazione tra task.