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

  1. Minden elindított szál handle-je pontosan egyszer eljut a join műveletig, hibás feladat mellett is.
  2. A feladathiba és a szálpánik külön ág, az eredmények sorrendje dokumentált és ellenőrzött.
  3. 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.