Cél és előfeltétel
A szálak több egymástól független munkát hajthatnak végre, de a befejezésük sorrendjét nem mi választjuk. A cél most nem sebességi ígéret, hanem az életciklus megértése: ki birtokolja az adatot, ki várja meg a munkát, és hogyan kerül vissza az eredmény. Előfeltétel a vektorok és szeletek, valamint a Result használata. Minden feladat kis, memóriabeli adatokkal dolgozik, és a helyességet állításokkal ellenőrzi.
A handle egy befejezési kötelezettség
A thread::spawn egy lezárást indít el új operációsrendszer-szálon, és JoinHandle értéket ad vissza. A join megvárja a hozzá tartozó szál végét, majd átadja annak visszatérési értékét vagy pánikját. A handle eldobása nem várakozás: a szál leválik, és az eredményét ezen az úton már nem vesszük át. A példák ezért minden létrehozott handle-t összegyűjtenek és lezárnak.
A move lezárás a felhasznált értékeket saját környezetébe mozgatja. Egy String vagy Vec tulajdona így átkerülhet a munkát végző szálhoz. Másolható kis számoknál másolat készül. Ez nem teszi minden adatot automatikusan szálbiztossá: a küldött értékeknek a megfelelő Send követelményt is teljesíteniük kell. Az adatok felosztásának és az eredmény visszaadásának terve továbbra is a programozó feladata.
A szokásos spawn lezárása nem kölcsönözhet tetszőleges helyi változót, mert az indító függvény előbb is visszatérhetne. A 'static típusmegkötés nem azt jelenti, hogy egy tulajdonolt vektort örökké életben kell tartani. Azt zárja ki, hogy a szál rövidebb életű külső kölcsönzésre támaszkodjon. A következő leckében erre ad más megoldást a hatókörhöz kötött szálkezelés.
1. feladat: tulajdon átadása és visszatérő összeg
Mozgass egy vektort a szálba, és az összegét a lezárás visszatérési értékeként add át. Ne közös változóba írj, és ne terminálkimenetből olvasd vissza az eredményt. Az üres bemenetet külön hívással ellenőrizd. A tesztadatok összege biztosan belefér az alkalmazott egész típusba; itt a tulajdon és a várakozás a vizsgált szerződés.
Megoldás
use std::thread;
fn sum_on_thread(values: Vec<u32>) -> u32 {
let handle = thread::spawn(move || values.into_iter().sum());
handle.join().expect("az összegző szál nem pánikolhat")
}
fn main() {
assert_eq!(sum_on_thread(vec![2, 3, 5]), 10);
assert_eq!(sum_on_thread(vec![]), 0);
}
A feladat hibája nem szálpánik
Ha a lezárás Result<T, E> értéket ad, a join további réteget hoz létre. Az Ok(Err(e)) azt mondja: a szál szabályosan lefutott, de a feladat elutasította a bemenetet. Az Err kívülről a szál pánikját jelzi. Ezeket összemosva a rossz felhasználói adatot könnyű infrastruktúrahibának nevezni. A pánik önmagában nem állítja vissza más szálak állapotát, és nem ad üzleti visszagörgetést.
2. feladat: hibázható szálmunka
Egy szál szövegből olvasson egész számot. A hibás adatot saját, rövid feladathibává alakítsd, miközben a join eredményét külön bontod ki. A sikeres és a hibás bemenetnél is végig kell futnia a várakozásnak. Az üres szöveg ugyanolyan szabályos elutasítás, mint a betűkből álló adat.
Megoldás
use std::thread;
fn read(text: String) -> Result<u32, &'static str> {
let handle = thread::spawn(move || text.parse::<u32>().map_err(|_| "számhiba"));
match handle.join() {
Ok(result) => result,
Err(_) => Err("szálpánik"),
}
}
fn main() {
assert_eq!(read(String::from("42")), Ok(42));
assert_eq!(read(String::from("sok")), Err("számhiba"));
assert_eq!(read(String::new()), Err("számhiba"));
}
3. feladat: minden munka lezárása hiba mellett is
Indíts több számon négyzetképzést. Az eredményeket a bemeneti sorrendben gyűjtsd vissza, és a túl nagy számot feladathibaként tartsd meg. Egy hibás eredmény ne szakítsa meg a többi handle feldolgozását. A megoldás nem figyeli, melyik szál ér előbb célba: a handle-ek vektora határozza meg a visszaadott sorrendet.
Megoldás
use std::thread;
fn squares(values: Vec<u32>) -> Vec<Result<u32, &'static str>> {
let handles: Vec<_> = values.into_iter().map(|n| {
thread::spawn(move || n.checked_mul(n).ok_or("túlcsordulás"))
}).collect();
handles.into_iter().map(|h| match h.join() {
Ok(result) => result,
Err(_) => Err("szálpánik"),
}).collect()
}
fn main() {
assert_eq!(squares(vec![3, u32::MAX, 2]),
vec![Ok(9), Err("túlcsordulás"), Ok(4)]);
assert!(squares(vec![]).is_empty());
}
Ne az ütemezőt teszteld
Egy előre beállított várakozás nem bizonyítja, hogy a másik szál elkészült. A terminálra írt sorok sorrendje sem stabil szinkronizációs eszköz. A példák a join befejezési garanciájára támaszkodnak, és csak ezután vizsgálják az adatokat. Ugyanez teszi őket megismételhetővé lassú vagy erősen terhelt gépen is. A sok apró szál indítása itt oktatási eszköz; nagy bemenetnél külön munkaszám-korlátot kell tervezni.
Az eredmény rendezett visszagyűjtése és a munkák tényleges átfedése nem zárja ki egymást. A harmadik feladat előbb létrehozza az összes szálat, majd sorban vár a handle-ekre. Ha az első lassabb lenne, a többi közben már befejezhetné a saját munkáját. A rendezett várakozás tehát nem teszi egymás utánivá az előzetesen elindított számításokat. Arra viszont figyelni kell, hogy a vektorban minden handle megmaradjon: egy korai hibás visszatérés külön leállítási terv nélkül elveszítené a megvárás kötelezettségét.
A feladat teljes eredményét csak a megfelelő handle bevárása után használd fel. A szál puszta elindítása még nem bizonyítja a munka sikeres befejezését.
Elfogadási feltételek
- Minden elindított szál handle-je pontosan egyszer eljut a
joinműveletig, hibás feladat mellett is. - A feladathiba és a szálpánik külön ág, az eredmények sorrendje dokumentált és ellenőrzött.
- Az üres bemenet és az aritmetikai túlcsordulás tesztje nem használ alvást vagy időmérést.
Továbblépés
Rajzold le, melyik érték melyik szál tulajdona a hívás előtt és után. A scoped szálak leckéjében ugyanezt a befejezési fegyelmet úgy őrizzük meg, hogy közben helyi szeleteket kölcsönzünk. Ez akkor hasznos, ha az adatok már az indító függvényben vannak.