async și await în Rust

Cod unsafe, FFI și async Rust 1.39 (Waker::noop: 1.85; async closures: 1.85)

async creează funcții și blocuri care returnează un Future; .await așteaptă un Future fără să blocheze firul. Un executor rulează totul.

Sintaxă
async fn nume(...) -> Tip { ... }        // returnează un Future<Output = Tip>
let f = async { ... };                    // bloc async
let rezultat = alt_future.await;          // așteaptă un future (doar în cod async)
// rularea cere un executor: tokio::main, async_std, futures::executor::block_on

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: main nu poate fi async în std. Folosește #[tokio::main] (sau alt runtime) ori un block_on.
  • Folosești std::thread::sleep sau 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.