Az előző leckében láttad, hogy egy Future valójában egy állapotgép, amit valakinek pollozni kell ahhoz, hogy előrébb jusson. Ez kézzel implementálva gyorsan fárasztóvá válik: minden lépésnél neked kellene nyilvántartanod, hol jársz, és mit kell csinálni a következő poll-nál. Az async/await szintaxis pontosan ezt a terhet veszi le rólad – a fordító generálja helyetted a Future-t leíró állapotgépet, neked pedig csak úgy kell megírnod a kódot, mintha egyenes vonalú, szinkron lenne. Ez a lecke arról szól, hogyan néz ki ez a szintaxis, és mit csinál valójában a háttérben.
Az async fn szintaxis
Ha egy függvény elé odaírod az async kulcsszót, a függvény nem a megszokott módon fut le meghívásakor. Ehelyett a hívás azonnal visszatér egy Future értékkel, és a függvény teste csak akkor kezd el futni, amikor ezt a Future-t valaki pollozza.
async fn greet(name: &str) -> String {
format!("Szia, {name}!")
}
fn main() {
// Az async fn hívása NEM futtatja le azonnal a függvény testét!
let future = greet("Rust");
// A `future` egy Future<Output = String> érték, amit még senki nem pollozott.
// Ha itt egyszerűen eldobjuk, a `format!` a `greet` belsejében sosem fut le.
drop(future);
}
Ez a program lefordul és lefut, de a greet függvény teste valójában soha nem hajtódik végre – nincs olyan kód, ami pollozná a future-t. Ez az egyik legfontosabb dolog, amit meg kell szoknod: egy async fn hívása csupán egy Future-t hoz létre, nem futtatja le a tartalmát.
A main függvény itt szándékosan sima, nem async. Egy futtatható async programhoz egy executor (futtató motor) szükséges, amit a következő leckében a Tokio-val ismersz meg.
A .await operátor
Ha egy Future eredményére szükséged van, a .await operátorral tudod megvárni és kiolvasni azt – de csak akkor, ha te is async kontextusban vagy, azaz egy async fn vagy async blokk belsejében.
async fn greet(name: &str) -> String {
format!("Szia, {name}!")
}
async fn fetch_greeting(name: &str) -> String {
// A `.await` megvárja, amíg a belső future eredménye elkészül,
// és kibontja azt - a poll ciklust a fordító generálja a háttérben.
let message = greet(name).await;
format!("{message} 👋")
}
fn main() {
let _future = fetch_greeting("Rust");
// Ezt a future-t sem futtatjuk le itt - erről a következő leckében lesz szó.
}
Fontos, hogy a .await nem egy szál-blokkoló várakozás, hanem szintaktikai jelzés a fordítónak: „itt kell egy poll pontot beszúrni, és ha a Future még nem áll készen, add vissza a vezérlést az executor-nak”. Emiatt nem is használható akárhol – csak async kontextusban van értelme.
fn main() {
let future = greet("Rust");
let result = future.await; // hiba: nem async fn-ben vagyunk
println!("{result}");
}
async fn greet(name: &str) -> String {
format!("Szia, {name}!")
}
error[E0728]: `await` is only allowed inside `async` functions and blocks
A fordító itt azért panaszkodik, mert a main nem async fn, tehát nincs értelme benne .await-elni – nincs, aki a poll ciklust levezetné.
async blokkok
Az async fn mellett létezik egy anonim forma is: az async blokk. Ez egy kifejezés, ami egy Future-t hoz létre, anélkül, hogy külön függvényt kellene deklarálnod hozzá.
fn main() {
// Egy async blokk egy Future-t hoz létre, függvény nélkül.
let future = async {
let name = "Rust";
format!("Szia, {name}!")
};
drop(future);
}
Ez különösen hasznos, ha egy meglévő async fn-en belül szeretnél egy kisebb, önálló Future-t kicsomagolni – például azért, hogy egy bizonyos értéket előre elmozgass (move) a blokkba, ahelyett hogy csak kölcsönöznéd.
async fn greet(name: String) -> String {
format!("Szia, {name}!")
}
async fn make_greeting() -> String {
let name = String::from("Rust");
// Az `async move` blokk átveszi a `name` tulajdonjogát,
// így a belső future önmagában is érvényes marad, kölcsönzés nélkül.
let future = async move { greet(name).await };
future.await
}
fn main() {
let _future = make_greeting();
}
Az async move ugyanazt csinálja, mint amit a closure-öknél megszoktál a move kulcsszóval: a blokk körüli környezetből mozgatja be az értékeket (itt a name-et), nem csak kölcsönzi őket. Ez azért fontos, mert egy Future gyakran tovább él, mint az a scope, ahol létrehoztad – ha csak kölcsönözne, a borrow checker rögtön leállítana.
Send + 'static kitekintés
Amikor egy Future-t egy futtató motor (executor) egyszerre több szálon is mozgathat, két extra követelmény jön szóba: a Future-nek Send-nek és 'static-nak kell lennie.
- A
Sendazt jelenti, hogy az érték biztonságosan átadható egyik szálról a másikra – ha egy Future belsejében csakSendtípusok élnek (ami aString, a legtöbb primitív típus, és a legtöbb saját struct-od esetén igaz), akkor a Future maga isSendlesz. - A
'staticazt jelenti, hogy a Future nem tartalmaz olyan referenciát, amelynek élettartama rövidebb, mint a program teljes futása – tehát semmi nincs benne kölcsönözve egy hamarosan megszűnő scope-ból.
Ez most még csak elméleti fogalom marad – a következő leckében, amikor egy Future-t egy másik szálon futó taszkként indítasz el, pontosan ezek a Send + 'static feltételek lesznek azok, amiket a fordító ellenőriz.
Gyakori kezdő hibák
- Elfelejtett
.await: ha egy async fn hívása után nem.await-eled az eredményt, a Future sosem fut le – a fordító ilyenkor gyakran figyelmeztet egy „unused implementer of Future” jellegű üzenettel. - Azt hinni, hogy az async fn azonnal fut: a hívás csak létrehozza a Future-t, a tényleges munka a pollozáskor kezdődik – ahogy az első kódrészletben is láttuk.
.awaithasználata nem async kontextusban: ezt a fordító mindig elkapja (E0728 hiba), de érdemes tudni, miért – mert nincs, ami levezetné a poll ciklust.- Hosszú, blokkoló számítást tenni egy async fn-be
.awaitnélkül: ez ugyan lefordul, de amint egy executor több feladatot egyszerre próbál futtatni, egy blokkoló rész az egész szálat megbéníthatja – ezt a témát részletesebben a Tokio-leckéknél nézzük meg.
Próbáld ki!
Írj egy async fn summarize(count: u32) -> String függvényt, amely egy formázott szöveget ad vissza (pl. format!("{count} elem van a listában")). Majd írj egy másik async fn vagy async blokkot, amely .await-eli ezt, és összefűzi egy második mondattal. Fordítsd le cargo check-kel, és figyeld meg: bár a kód helyes, semmi nem „történik” futás közben, amíg nincs executor, ami pollozná a Future-t.
Önellenőrzés
Megoldás
Mi történik, ha meghívsz egy `async fn`-t, de az eredményét sosem `.await`-eled?A függvény teste egyáltalán nem fut le. A hívás csak egy Future értéket hoz létre, amit soha senki nem pollozott, tehát a benne lévő kód (pl. egy format! hívás) nem hajtódik végre.
Megoldás
Miért nem fordul le a `future.await` egy sima (nem `async`) `main` függvényben?Mert a .await csak async fn vagy async blokk belsejében engedélyezett – ott, ahol a fordító tudja generálni a szükséges poll-állapotgépet. Egy sima fn-ben nincs ilyen kontextus, ezért a fordító E0728 hibát dob.
Megoldás
Mi a különbség az `async fn` és az `async` blokk között?Az async fn egy elnevezett függvény, amelynek a visszatérési típusa implicit módon egy Future lesz. Az async blokk ezzel szemben egy kifejezés: bárhol a kódban létrehozhatsz vele egy anonim Future-t, függvénydeklaráció nélkül – ez különösen hasznos, ha egy async fn-en belül szeretnél egy kisebb, önállóan mozgatható (async move) Future-t kicsomagolni.
Összefoglalás és mi jön legközelebb
- Az
async fnhívása nem futtatja le azonnal a függvény testét, csak egy Future-t hoz létre. - A
.awaita poll ciklust rejti el – csakasynckontextusban használható, és nem blokkolja a szálat, hanem visszaadja a vezérlést az executornak, amíg a Future nincs készen. - Az
asyncblokkok anonim Future-öket hoznak létre; azasync moveblokk a bezárt értékek tulajdonjogát is átveszi, ahogy a closure-öknél megszokhattad. - A
Send + 'statickövetelmények azért fontosak, mert egy multi-threaded executor csak ilyen Future-öket tud biztonságosan más szálra mozgatni – ezt a következő leckében, a Tokio spawn és join témakörnél fogjuk éles kódban is látni. - Eddig minden Future-t csak létrehoztunk, de sosem futtattunk le valóban – ehhez egy executor szükséges, amit a Tokio: spawn és join című leckében ismersz meg.
Ha el akarsz mélyülni a témában, ezek jó kiinduló pontok: