Les 2 · Module 1 · Async en Axum-basis
Async Rust en Tokio
Begrijp wat async en await in Rust doen, hoe de Tokio-runtime taken verdeelt, en waarom je nooit blokkerende code in een handler zet.
🎯 Na deze les kun je
- uitleggen wat een future is en wanneer hij wordt uitgevoerd
- taken starten met tokio::spawn en samenvoegen met join!
- blokkerend werk verplaatsen met spawn_blocking
- timeouts en select! toepassen
In les 1 schreef je async fn en .await zonder er veel over na te denken. Voor een webservice is het belangrijk te snappen wat
er gebeurt, want één verkeerde regel kan je hele service laten haperen. In deze les experimenteer je in een apart voorbeeld en pas
je het daarna toe op kassa-api.
Futures zijn lui
async fn zeg(tekst: &str) { println!("{tekst}"); }
#[tokio::main]
async fn main() {
let f = zeg("hallo"); // er gebeurt nog niets
println!("eerst dit");
f.await; // nu pas wordt "hallo" geprint
}
Een async fn geeft een future terug: een beschrijving van werk. Pas .await (of een runtime die hem uitvoert) laat het werk
gebeuren. Bij elke .await waar nog niets klaar is, geeft de future de beurt terug aan de runtime, die dan iets anders doet.
De runtime
Tokio heeft een paar worker-threads (standaard één per CPU-kern) en verdeelt daarover duizenden taken. Elk binnenkomend verzoek is een taak. Wacht een taak op het netwerk of de database, dan draait de thread een andere taak. Zo bedient een kleine service veel gelijktijdige verzoeken.
Gelijktijdig wachten
use std::time::{Duration, Instant};
use tokio::time::sleep;
async fn haal(naam: &str, ms: u64) -> String { sleep(Duration::from_millis(ms)).await; naam.to_string() }
#[tokio::main]
async fn main() {
let t = Instant::now();
let a = haal("voorraad", 300).await;
let b = haal("prijzen", 300).await;
println!("na elkaar: {:?}", t.elapsed());
let t = Instant::now();
let (a2, b2) = tokio::join!(haal("voorraad", 300), haal("prijzen", 300));
println!("tegelijk: {:?}", t.elapsed());
}
Voorspel eerst: welke tijden zie je ongeveer?
Uitkomst
Na elkaar ±600 ms, tegelijk ±300 ms. join! laat beide futures in dezelfde taak om de beurt vooruitgaan; terwijl de ene slaapt, slaapt
de andere ook. Gebruik je in plaats van tokio::time::sleep echter std::thread::sleep, dan is ook de join!-versie 600 ms: die blokkeert
de thread en geeft de beurt niet terug.
Taken starten
let handle = tokio::spawn(async move {
// draait als losse taak, mogelijk op een andere thread
tel_dagomzet().await
});
let omzet = handle.await.unwrap(); // JoinHandle; Err als de taak panickte
Omdat een gespawnde taak naar een andere thread kan gaan, moet alles wat erin zit Send + 'static zijn: gebruik move en geef eigendom
mee (of een Arc, les 6).
Nooit blokkeren
Blokkerend werk (zware berekening, std::fs, een synchrone bibliotheek, std::thread::sleep) houdt een worker-thread bezet. Met 8
threads en 8 trage verzoeken staat je hele service stil. Verplaats het:
let hash = tokio::task::spawn_blocking(move || bcrypt_achtige_hash(&wachtwoord)).await.unwrap();
spawn_blocking draait de closure op een aparte pool voor blokkerend werk. Voor bestanden heeft Tokio tokio::fs.
Timeouts en select!
use tokio::time::{timeout, Duration};
match timeout(Duration::from_secs(2), haal_koers()).await {
Ok(koers) => println!("{koers}"),
Err(_) => println!("te traag, gebruik de vorige koers"),
}
tokio::select! {
r = haal_van_server_a() => println!("A was eerst: {r}"),
r = haal_van_server_b() => println!("B was eerst: {r}"),
}
select! wacht op de eerste die klaar is en annuleert de andere (de future wordt gedropt).
Toepassen op kassa-api
Voeg een endpoint toe dat twee "trage bronnen" tegelijk raadpleegt:
async fn dashboard() -> Json<serde_json::Value> {
let (open, omzet) = tokio::join!(open_bestellingen(), dagomzet());
Json(serde_json::json!({ "open": open, "omzet_centen": omzet }))
}
Typische fouten
future cannot be sent between threads safely: je houdt een niet-Sendwaarde (zoalsRcof eenstd::sync::MutexGuard) vast over een.await..awaitvergeten: de compiler waarschuwtunused implementer of Future; het werk gebeurt dan nooit.- Een
std::sync::Mutexvasthouden over.await: kan vastlopen. Laat de guard los vóór de.await(les 6).
Oefenen
- Draai het
join!-voorbeeld en vervang daarnatokio::time::sleepdoorstd::thread::sleep. Verklaar het verschil. - Start met
tokio::spawntien taken die elk een willekeurige tijd slapen en hun nummer teruggeven; verzamel de resultaten. - Maak in kassa-api
/dashboardmetjoin!en zorg mettimeoutdat het nooit langer dan 1 seconde duurt. - Bereken in een handler de som van 1 tot 200 miljoen. Wat gebeurt er met andere verzoeken? Los het op met
spawn_blocking.
Modelantwoord oefening 1
Met std::thread::sleep blokkeert de eerste future de thread 300 ms zonder de beurt terug te geven; pas daarna begint de tweede. Totaal 600 ms.
Modelantwoord oefening 2
let mut handles = Vec::new();
for i in 0..10 {
handles.push(tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(50 * (10 - i))).await;
i
}));
}
let mut uit = Vec::new();
for h in handles { uit.push(h.await.unwrap()); }
println!("{uit:?}"); // [0, 1, ..., 9]: volgorde van awaiten, niet van klaar zijn
Modelantwoord oefening 3
async fn dashboard() -> Result<Json<serde_json::Value>, StatusCode> {
let (open, omzet) = timeout(Duration::from_secs(1), async { tokio::join!(open_bestellingen(), dagomzet()) })
.await.map_err(|_| StatusCode::GATEWAY_TIMEOUT)?;
Ok(Json(serde_json::json!({ "open": open, "omzet_centen": omzet })))
}
Modelantwoord oefening 4
In debugmodus duurt de som seconden en houdt hij een worker-thread bezet; met weinig kernen reageren andere verzoeken traag. Oplossing:
let som = tokio::task::spawn_blocking(|| (1..=200_000_000u64).sum::<u64>()).await.unwrap();
Lokaal oefenen
Geen live lab. Maak voor de experimenten een los project (cargo new async-proef) met tokio (features full).
Samenvatting
- Futures zijn lui;
.awaitvoert ze uit en geeft de beurt terug als er gewacht moet worden. - Tokio verdeelt taken over een paar threads;
join!wacht tegelijk,spawnmaakt losse taken. - Nooit blokkeren in async code: gebruik
spawn_blockingof async-varianten. timeoutenselect!begrenzen en kiezen.- Volgende les: routing en handlers.