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.

Megjegyzés

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 Send azt jelenti, hogy az érték biztonságosan átadható egyik szálról a másikra – ha egy Future belsejében csak Send típusok élnek (ami a String, a legtöbb primitív típus, és a legtöbb saját struct-od esetén igaz), akkor a Future maga is Send lesz.
  • A 'static azt 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.
Megjegyzés

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.
  • .await haszná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 .await né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 fn hívása nem futtatja le azonnal a függvény testét, csak egy Future-t hoz létre.
  • A .await a poll ciklust rejti el – csak async kontextusban használható, és nem blokkolja a szálat, hanem visszaadja a vezérlést az executornak, amíg a Future nincs készen.
  • Az async blokkok anonim Future-öket hoznak létre; az async move blokk a bezárt értékek tulajdonjogát is átveszi, ahogy a closure-öknél megszokhattad.
  • A Send + 'static kö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: