Ce face async și await
O funcție async fn nu își execută corpul când o apelezi: returnează imediat un Future, o valoare care descrie o lucrare ce va produce un rezultat mai târziu. Abia când cineva face .await pe el (sau un executor îl rulează), codul începe. În interiorul codului async, .await așteaptă un alt future; dacă acesta nu este gata (de exemplu așteaptă date din rețea), firul este eliberat pentru alte sarcini.
Biblioteca standard definește doar mecanismul (Future, Poll, Context), nu și un executor care rulează future-urile. În aplicații reale folosești un crate precum tokio (#[tokio::main] async fn main()), care oferă executorul, timere, rețea și fișiere async. Exemplele de mai jos folosesc doar std și un executor minimal scris în câteva rânduri, ca să vezi ce se întâmplă.
Folosește async pentru multe operații care așteaptă (mii de conexiuni de rețea). Pentru calcule care țin procesorul ocupat, firele de execuție (std::thread) sunt mai potrivite. Cuvintele async și await sunt rezervate din ediția 2018.
Exemple
Exemplul 1: Un future nu face nimic până nu este așteptat
block_on este un executor minimal: apelează poll până când future-ul este gata. Ordinea mesajelor arată că apelul pregateste_comanda(1) nu execută nimic; corpul rulează abia la .await.
use std::pin::pin;
use std::task::{Context, Poll, Waker};
fn block_on<F: Future>(future: F) -> F::Output {
let mut future = pin!(future);
let mut cx = Context::from_waker(Waker::noop());
loop {
if let Poll::Ready(valoare) = future.as_mut().poll(&mut cx) {
return valoare;
}
}
}
async fn pregateste_comanda(nr: u32) -> String {
println!(" încep comanda {nr}");
format!("comanda {nr} gata")
}
async fn program() {
let f = pregateste_comanda(1);
println!("am creat future-ul, dar nu a rulat încă");
let rezultat = f.await;
println!("{rezultat}");
}
fn main() {
block_on(program());
}
Rezultat
am creat future-ul, dar nu a rulat încă
încep comanda 1
comanda 1 gata
Exemplul 2: Funcții async care se așteaptă una pe alta
total_cos așteaptă pe rând prețul fiecărui produs. Blocul async move { ... } este tot un future, rulat de același executor.
use std::pin::pin;
use std::task::{Context, Poll, Waker};
fn block_on<F: Future>(future: F) -> F::Output {
let mut future = pin!(future);
let mut cx = Context::from_waker(Waker::noop());
loop {
if let Poll::Ready(valoare) = future.as_mut().poll(&mut cx) {
return valoare;
}
}
}
async fn pret(produs: &str) -> f64 {
match produs {
"pâine" => 5.5,
"lapte" => 9.0,
_ => 0.0,
}
}
async fn total_cos(produse: &[&str]) -> f64 {
let mut total = 0.0;
for p in produse {
total += pret(p).await;
}
total
}
fn main() {
let cos = vec!["pâine", "lapte", "lapte"];
let total = block_on(async move {
let t = total_cos(&cos).await;
format!("{} produse, total {t:.2} lei", cos.len())
});
println!("{total}");
}
Rezultat
3 produse, total 23.50 lei
Exemplul 3: Cum arată cu tokio (crate extern)
Într-un proiect real, executorul vine din tokio (cargo add tokio --features full). tokio::join! rulează cele două descărcări în același timp: durata totală este a celei mai lungi, nu suma lor.
use std::time::{Duration, Instant};
async fn descarca(nume: &str, ms: u64) -> String {
tokio::time::sleep(Duration::from_millis(ms)).await;
format!("{nume} descărcat")
}
#[tokio::main]
async fn main() {
let start = Instant::now();
let (a, b) = tokio::join!(descarca("raport.pdf", 300), descarca("poza.jpg", 200));
println!("{a}, {b}");
println!("Durata: ~{} ms", start.elapsed().as_millis() / 100 * 100);
}
Rezultat (cu crate-ul tokio)
raport.pdf descărcat, poza.jpg descărcat
Durata: ~300 ms
Greșeli frecvente
- Apelezi o funcție
asyncși uiți.await: nu se întâmplă nimic, iar compilatorul avertizeazăunused implementer of `Future` that must be used. - Scrii
async fn main()fără un executor:mainnu poate fiasyncînstd. Folosește#[tokio::main](sau alt runtime) ori unblock_on. - Folosești
std::thread::sleepsau citiri blocante într-o funcție async: blochezi tot firul executorului și toate celelalte sarcini. Folosește variantele async ale runtime-ului (tokio::time::sleep,tokio::fs).
Comenzi înrudite
closures și move: Funcții anonime, scrise cu |parametri|, care pot folosi variabilele din jur. move le dă proprietatea asupra acelor variabile.trait: Definește un set de metode pe care un tip promite să le aibă. Seamănă cu interfețele din alte limbaje și stă la baza generics.impl Trait: Scrie „un tip care implementează Trait” fără să-l numești: ca parametru este o scurtătură pentru generics, ca rezultat ascunde tipul exact.cargo add: Adaugă o dependență (un crate de pe crates.io) în Cargo.toml, cu ultima versiune compatibilă. cargo remove o scoate.