Az előző leckében megismerted az async fn-eket, a .await-et és az async blokkokat — de egyelőre csak egyetlen Future-t tudtál futtatni egyszerre, egymás után. Ideje ezen továbblépni: ma megtanulod, hogyan futtathatsz több async feladatot egyszerre a Tokio runtime segítségével.
Cél és előfeltételek
Ez a lecke egy önálló, kis Cargo-projektet épít fel lépésről lépésre. A végére képes leszel:
- elindítani a Tokio async runtime-ot a
#[tokio::main]makróval, - konkurrens taskokat indítani a
tokio::spawnsegítségével, - egy elindított task eredményét visszavárni egy
JoinHandle-lel, - több Future-t egyszerre várakoztatni a
join!makróval, - az elsőként befutó Future-t kiválasztani a
select!makróval.
Ehhez szükséged lesz a korábbi leckék anyagára: az async fn/.await szintaxisra, a Future trait fogalmi ismeretére, a Result/? operátorra, valamint az ownership- és borrow-szabályokra (ezek async környezetben is ugyanúgy érvényesek).
A Send + 'static kitekintést az előző leckéből már ismered fogalmi szinten: a tokio::spawn-nal indított taskoknak ezeket a feltételeket kell teljesíteniük, mert a runtime bármikor átteheti őket egy másik szálra. Ma ezt a gyakorlatban is látni fogod.
Előkészítés
Hozz létre egy új Cargo-projektet, és add hozzá a Tokio függőséget.
cargo new tokio-spawn-join
cd tokio-spawn-join
[package]
name = "tokio-spawn-join"
version = "0.1.0"
edition = "2024"
[dependencies]
tokio = { version = "1", features = ["full"] }
A "full" feature-készlet most a legegyszerűbb választás: tartalmazza a runtime-ot, a spawn-t, az időzítést (tokio::time) és a makrókat is. Élesben érdemes lesz majd finomabban megválasztani a feature-öket, de gyakorláshoz ez most tökéletes.
Ellenőrizd, hogy fordul-e a projekt:
cargo check
1. lépés — a #[tokio::main] makró
Ez a makró teszi lehetővé, hogy a main függvényed async fn legyen. A háttérben egy teljes Tokio runtime-ot indít el, elindítja a main-t mint egy Future-t, és lefuttatja azt a végéig.
Feladat: írj egy programot, amelyben a main async fn, és belül .await-tel hívsz meg egy másik async fn-t, amely tokio::time::sleep-pel szimulál egy kis várakozást, majd kiír egy üzenetet.
Megoldás
use std::time::Duration;
async fn greet(name: &str) {
tokio::time::sleep(Duration::from_millis(100)).await;
println!("Szia, {name}!");
}
#[tokio::main]
async fn main() {
greet("Rust tanuló").await;
println!("A program végére értünk.");
}
A #[tokio::main] alapértelmezetten egy multi-thread runtime-ot indít, ami több OS-szálon ütemezi a taskokat. Ha csak egy szálat szeretnél (pl. teszteléshez), írhatod #[tokio::main(flavor = "current_thread")] formában is — ezt egyelőre elég csak megjegyezni.
2. lépés — tokio::spawn és JoinHandle
A .await önmagában nem konkurrens: amíg egy Future-re várunk, a kódunk ott áll meg, és csak azután folytatódik. A konkurrenciához a tokio::spawn függvény kell: ez egy Future-t (jellemzően egy async blokk vagy async fn eredményét) elad a runtime-nak, amely azt függetlenül, a háttérben futtatja.
A tokio::spawn visszatérési értéke egy JoinHandle<T>, ahol T a taskod visszatérési típusa. A JoinHandle maga is .await-elhető: amikor megvárod, visszakapod a task eredményét egy Result<T, JoinError> csomagolásban (a JoinError akkor jönne létre, ha a task belül panicolna — ezt egyelőre elég tudni, hogy létezik).
Feladat: indíts el egy taskot tokio::spawn-nal, amely visszaad egy i32 értéket, majd várd vissza a JoinHandle-lel, és írd ki az eredményt.
Megoldás
use std::time::Duration;
async fn compute() -> i32 {
tokio::time::sleep(Duration::from_millis(50)).await;
42
}
#[tokio::main]
async fn main() {
let handle = tokio::spawn(compute());
// Itt közben a main task is dolgozhatna valamin.
println!("A task elindult, várunk az eredményre...");
match handle.await {
Ok(value) => println!("A task eredménye: {value}"),
Err(err) => println!("A task hibával állt le: {err}"),
}
}
A tokio::spawn-nal indított Future-nek 'static élettartamúnak és Send-nek kell lennie. Ez azt jelenti, hogy nem tartalmazhat rövid életű referenciákat a spawn-t hívó függvény lokális változóira — a taskba mozgatott (move) adatoknak saját tulajdonban kell lenniük. Ha egy változót meg akarsz osztani több task között, a Rust fordító azonnal jelezni fogja, ha ez sérülne.
3. lépés — több task egyszerre
A tokio::spawn igazi ereje akkor látszik, amikor több taskot indítasz egyszerre, és azok párhuzamosan (valójában: konkurrensen, egymással átfedve) futnak.
Feladat: indíts el egy ciklusban öt taskot, mindegyik szimuláljon egy kis munkát (sleep), és adjon vissza egy számot. Gyűjtsd össze a JoinHandle-öket egy Vec-be, majd várd vissza mindegyiket, és összesítsd az eredményeket.
Megoldás
use std::time::Duration;
async fn work(id: u32) -> u32 {
tokio::time::sleep(Duration::from_millis(50)).await;
id * 10
}
#[tokio::main]
async fn main() {
let mut handles = Vec::new();
for id in 1..=5 {
// A `move` szükséges, mert az `id`-t a taskba mozgatjuk.
let handle = tokio::spawn(async move { work(id).await });
handles.push(handle);
}
let mut sum = 0;
for handle in handles {
let result = handle.await.expect("a task ne paniceljon");
println!("Eredmény: {result}");
sum += result;
}
println!("Összesen: {sum}");
}
Figyeld meg: mind az öt task egyszerre elindul, és mivel mindegyik ugyanannyit (50 ms-ot) vár, a teljes program mégis csak nagyjából 50 ms-ot vesz igénybe — nem 250-et. Ez a konkurrencia gyakorlati haszna.
4. lépés — a join! makró
Ha nem szeretnél JoinHandle-öket kézzel gyűjtögetni, hanem csak egyszerűen egyszerre meg akarsz várni néhány Future-t (ugyanabban a taskban, spawn nélkül), erre a tokio::join! makró a legkézenfekvőbb eszköz. A join! a megadott Future-öket egymással átfedve futtatja, és egy tuple-ben adja vissza az összes eredményt, miután mindegyik befutott.
Feladat: hívj meg egyszerre két különböző async fn-t a join! makróval, amelyek eltérő típust adnak vissza (pl. egy szám és egy szöveg), és írd ki mindkét eredményt.
Megoldás
use std::time::Duration;
async fn fetch_number() -> i32 {
tokio::time::sleep(Duration::from_millis(30)).await;
7
}
async fn fetch_message() -> String {
tokio::time::sleep(Duration::from_millis(60)).await;
String::from("kész vagyok")
}
#[tokio::main]
async fn main() {
let (number, message) = tokio::join!(fetch_number(), fetch_message());
println!("Szám: {number}, üzenet: {message}");
}
A join! nem indít külön taskot, mint a tokio::spawn — a megadott Future-ök ugyanazon a taskon belül futnak, csak felváltva haladnak, amikor egyikük vár (pl. sleep közben a másik is haladhat). Ha valódi, más szálon futó párhuzamosságot akarsz, a tokio::spawn a megfelelő eszköz.
5. lépés — a select! alapjai
Néha nem az összes Future eredményére van szükséged, csak arra, amelyik elsőként befut — a többit pedig el akarod dobni. Erre a tokio::select! makró szolgál: több Future-t egyszerre figyel, és amint az egyik befejeződik, a hozzá tartozó ág kódját futtatja le, a többi Future pedig megszakad.
Feladat: indíts két sleep-elő Future-t eltérő várakozási idővel, és a select!-tel írd ki, melyik ért véget előbb.
Megoldás
use std::time::Duration;
async fn fast() -> &'static str {
tokio::time::sleep(Duration::from_millis(20)).await;
"gyors"
}
async fn slow() -> &'static str {
tokio::time::sleep(Duration::from_millis(200)).await;
"lassú"
}
#[tokio::main]
async fn main() {
tokio::select! {
result = fast() => {
println!("Előbb ez futott be: {result}");
}
result = slow() => {
println!("Előbb ez futott be: {result}");
}
}
println!("A select! után folytatódik a program.");
}
A példában a fast() fut be előbb, ezért a slow() Future futása megszakad — soha nem is íródik ki a "lassú" szöveg. Ez különösen hasznos időtúllépések (timeout) kezelésénél: egy select!-ben versenyeztetheted a valódi munkát egy sleep-pel, és ha a sleep fut be előbb, tudod, hogy túllépted az időkorlátot.
A select! ágakban használt Future-öknek Unpin-eknek kell lenniük, vagy Box::pin-nel rögzítve kell átadni őket, ha valamiért nem azok. A leckében szereplő async fn-ek automatikusan megfelelnek ennek, így ezzel most nem kell foglalkoznod — csak jegyezd meg, hogy bonyolultabb esetekben előjöhet.
Teljes referencia-megoldás
Az alábbi program egyben tartalmazza mindegyik lépés lényegét: taskok indítása spawn-nal, eredmények visszavárása JoinHandle-lel, két Future egyidejű várakoztatása join!-lel, és egy verseny lezárása select!-tel.
use std::time::Duration;
async fn work(id: u32) -> u32 {
tokio::time::sleep(Duration::from_millis(30 * id as u64)).await;
id * 10
}
async fn fetch_number() -> i32 {
tokio::time::sleep(Duration::from_millis(30)).await;
7
}
async fn fetch_message() -> String {
tokio::time::sleep(Duration::from_millis(60)).await;
String::from("kész vagyok")
}
async fn fast() -> &'static str {
tokio::time::sleep(Duration::from_millis(20)).await;
"gyors"
}
async fn slow() -> &'static str {
tokio::time::sleep(Duration::from_millis(200)).await;
"lassú"
}
#[tokio::main]
async fn main() {
// 1) Több task indítása és összegyűjtése
let mut handles = Vec::new();
for id in 1..=3 {
handles.push(tokio::spawn(async move { work(id).await }));
}
let mut sum = 0;
for handle in handles {
sum += handle.await.expect("a task ne paniceljon");
}
println!("Taskok összesített eredménye: {sum}");
// 2) Két eltérő típusú Future egyidejű várakoztatása
let (number, message) = tokio::join!(fetch_number(), fetch_message());
println!("join! eredménye: {number} / {message}");
// 3) Az elsőként befutó Future kiválasztása
tokio::select! {
result = fast() => println!("select! győztese: {result}"),
result = slow() => println!("select! győztese: {result}"),
}
}
Ez a program egyetlen #[tokio::main] alatt bemutatja mind a négy fő eszközt, amelyeket ma megtanultál. Futtasd le, és próbáld megváltoztatni a sleep időtartamokat — figyeld meg, hogyan változik a kimenet sorrendje.
Bónusz-kihívások
- Módosítsd a taskokat indító ciklust úgy, hogy minden task egy
Result<u32, String>-et adjon vissza (pl. azid > 100esetén hibát), és a fő függvényben számold össze, hány task sikeres és hány hibás. - Írj egy
select!-es megoldást, amely egy valódi munkát végző Future-t versenyeztet egytokio::time::sleep-pel mint időtúllépéssel, és ha asleepgyőz, írj ki egy "időtúllépés" hibaüzenetet aprintln!-nel.
Összefoglalás és mi jön legközelebb
Ma megtanultad, hogyan indítja el a #[tokio::main] a Tokio runtime-ot, hogyan indítasz konkurrens taskokat tokio::spawn-nal, hogyan várod vissza az eredményüket JoinHandle-lel, és hogyan várakoztatsz vagy versenyeztetsz több Future-t a join! és select! makrókkal. Ezek az alapkövei minden komolyabb async Rust programnak.
Amit még nem érintettünk: hogyan kommunikálnak egymással a taskok futás közben, ahelyett hogy csak a végén adnának vissza egy értéket. A következő leckében ezt vesszük górcső alá: megismerkedünk a csatornákkal (mpsc és oneshot), amelyekkel a taskok üzeneteket küldhetnek egymásnak, amíg futnak.