Cél és előfeltétel

A miniprojekt memóriában kapott számlistákat dolgoz fel. Minden lista önálló munka, saját sorszámmal és ellenőrzött részösszeggel. Legfeljebb négy munkásszál indulhat, az eredménycsatornában legfeljebb két üzenet várakozik. A korlátos csatornák és a szálak lezárása már ismerős; most egyetlen követhető adatútba rendezzük őket. Nincs fájlírás, felhasználói bemenet vagy időzítésre épülő ellenőrzés.

Külön terv a munkára és az eredményre

A munkákat körkörösen osztjuk szét a munkások között. Ez egyszerű és determinisztikus, de nem ígér egyenlő futási időt: az egyik lista sokkal hosszabb lehet a másiknál. A feladat célja a tulajdon és a befejezés helyessége. Dinamikus munkasor bevezetése új szinkronizációs kérdéseket nyitna, ezért azt itt nem rejtjük el néhány véletlenszerűen megosztott változó mögött.

Minden munkás saját kosarat birtokol, így a bemeneti adatokhoz nem kell közös mutex. Az eredményeket egyetlen fogadó gyűjti. A sorszámot még a szálindítás előtt a bemenetből állítjuk elő, ezért utólag ellenőrizhető, hogy minden munka pontosan egyszer szerepel-e. A rendezés nem a befejezés idejét rekonstruálja, hanem a bemenet eredeti sorrendjét állítja helyre.

Három hibaszintet különböztetünk meg. A hibás munkásszám vagy a szál rendellenes befejezése a teljes futás hibája. Egy részösszeg túlcsordulása a megfelelő sor eredménye marad. Végül a sikeres részösszegek együtt is túl nagyok lehetnek a végösszeghez. Emiatt a jelentés a sorokat és a végösszeg saját Result értékét egyaránt megtartja; egy elutasított összesítés nem tünteti el a már elkészült részleteket.

1. feladat: stabil munkakiosztás

Készíts kosarakat egytől négyig megengedett munkásszámhoz. A sorszám a bemenet bejárási sorrendjéből származzon. Három munka két kosárnál a nulladik és második munkát az első kosárba, az elsőt a másodikba helyezi. Üres bemenetnél is ugyanannyi üres kosár készüljön. A nulla munkásszámot még az osztási művelet előtt utasítsd el.

Megoldás
type Job = (usize, Vec<u64>);
fn partition(jobs: Vec<Vec<u64>>, workers: usize) -> Result<Vec<Vec<Job>>, &'static str> {
    if !(1..=4).contains(&workers) { return Err("munkásszám"); }
    let mut buckets = vec![Vec::new(); workers];
    for (id, values) in jobs.into_iter().enumerate() { buckets[id % workers].push((id, values)); }
    Ok(buckets)
}
fn main() {
    let buckets = partition(vec![vec![1], vec![2], vec![3]], 2).unwrap();
    assert_eq!(buckets, vec![vec![(0, vec![1]), (2, vec![3])], vec![(1, vec![2])]]);
    assert_eq!(partition(vec![], 2).unwrap(), vec![vec![], vec![]]);
    assert!(partition(vec![], 0).is_err());
    assert!(partition(vec![], 5).is_err());
}

A részfeladat maradjon tiszta művelet

Mielőtt szálak közé teszed a számítást, önálló függvényként ellenőrizd. Így a számtani hiba nem keveredik a csatorna lezárásával. Az üres lista részösszege nulla, a túlcsordulás viszont hiba. A try_fold első hibánál megáll az adott listán, de a következő munka feldolgozása tovább folytatható. Ezzel a helyi hiba határa egyértelmű marad.

2. feladat: egy kosár ellenőrzött eredményei

A kosár minden munkájához készíts azonosítót és részösszeget tartalmazó sort. Ne hagyd el a hibás sort, és ne cseréld a hibát nullára. Az első és utolsó teszt azt is ellenőrzi, hogy üres munka és üres munkalista eltérő szerkezet: előbbinél van eredménysor, utóbbinál nincs.

Megoldás
type Row = (usize, Result<u64, &'static str>);
fn batch(jobs: Vec<(usize, Vec<u64>)>) -> Vec<Row> {
    jobs.into_iter().map(|(id, values)| {
        let sum = values.into_iter().try_fold(0_u64, |a, b|
            a.checked_add(b).ok_or("részösszeg"));
        (id, sum)
    }).collect()
}
fn main() {
    assert_eq!(batch(vec![(0, vec![]), (1, vec![2, 3])]), vec![(0, Ok(0)), (1, Ok(5))]);
    assert_eq!(batch(vec![(4, vec![u64::MAX, 1]), (5, vec![7])]),
        vec![(4, Err("részösszeg")), (5, Ok(7))]);
    assert!(batch(vec![]).is_empty());
}

3. feladat: teljes futás és rendezett jelentés

Kapcsold össze a kiosztást, a szálakat és a két üzenetes csatornát. Előbb fogadj minden eredményt, utána vizsgáld meg az összes handle-t. Egyetlen szálhibánál se térj vissza a még hátralévő handle-ek bevárása előtt. A végén rendezz, ellenőrizd a sorok számát és azonosítóit, majd számold ki a végösszeget. A kétféle túlcsordulás külön tesztadatot kapjon.

Megoldás
use std::{sync::mpsc, thread};
type Row = (usize, Result<u64, &'static str>);
#[derive(Debug, PartialEq)]
struct Report { rows: Vec<Row>, total: Result<u64, &'static str> }
fn summarize(jobs: Vec<Vec<u64>>, workers: usize) -> Result<Report, &'static str> {
    if !(1..=4).contains(&workers) { return Err("munkásszám"); }
    let expected = jobs.len();
    let mut buckets = vec![Vec::new(); workers];
    for (id, values) in jobs.into_iter().enumerate() { buckets[id % workers].push((id, values)); }
    let (tx, rx) = mpsc::sync_channel(2);
    let handles: Vec<_> = buckets.into_iter().map(|bucket| {
        let sender = tx.clone();
        thread::spawn(move || -> Result<(), &'static str> {
            for (id, values) in bucket {
                let sum = values.into_iter().try_fold(0_u64, |a, b|
                    a.checked_add(b).ok_or("részösszeg"));
                sender.send((id, sum)).map_err(|_| "fogadó megszűnt")?;
            }
            Ok(())
        })
    }).collect();
    drop(tx);
    let mut rows: Vec<Row> = rx.into_iter().collect();
    let mut failed = false;
    for handle in handles {
        match handle.join() { Ok(Ok(())) => {}, _ => failed = true }
    }
    if failed { return Err("munkásszál"); }
    rows.sort_unstable_by_key(|row| row.0);
    if rows.len() != expected || rows.iter().enumerate().any(|(id, row)| id != row.0) {
        return Err("hiányos eredmény");
    }
    let total = rows.iter().try_fold(0_u64, |sum, row| {
        let value = row.1?;
        sum.checked_add(value).ok_or("végösszeg")
    });
    Ok(Report { rows, total })
}
fn main() {
    let normal = summarize(vec![vec![1, 2], vec![], vec![3]], 2).unwrap();
    assert_eq!(normal.rows, vec![(0, Ok(3)), (1, Ok(0)), (2, Ok(3))]);
    assert_eq!(normal.total, Ok(6));
    let local = summarize(vec![vec![u64::MAX, 1], vec![5]], 2).unwrap();
    assert_eq!(local.rows, vec![(0, Err("részösszeg")), (1, Ok(5))]);
    assert_eq!(local.total, Err("részösszeg"));
    let global = summarize(vec![vec![u64::MAX], vec![1]], 1).unwrap();
    assert_eq!(global.rows, vec![(0, Ok(u64::MAX)), (1, Ok(1))]);
    assert_eq!(global.total, Err("végösszeg"));
    assert_eq!(summarize(vec![], 4).unwrap(), Report { rows: vec![], total: Ok(0) });
    assert_eq!(summarize(vec![], 0), Err("munkásszám"));
    assert_eq!(summarize(vec![], 5), Err("munkásszám"));
}

A korlátok pontos jelentése

A program minden bemeneti listát előre megkap, és a jelentés sorait is megőrzi. A véges csatorna ezért nem teszi állandó memóriaigényűvé a teljes megoldást. A munkásszám viszont legfeljebb négy, függetlenül a feladatok számától. A sorok tartalmának és darabszámának ellenőrzése többet mond a helyességről, mint egy véletlenül megfelelő végösszeg, amelyből két egymást kiegyenlítő hiba akár rejtve is maradhatna.

A mintában a koordinátor és a munkások szerepe szándékosan aszimmetrikus. A munkás csak számol és küld, a koordinátor fogad, rendez és ellenőriz. Egy munkásnak nem kell ismernie más kosarak tartalmát vagy a végösszeget. Ez szűkíti azt az állapotot, amelyről egyszerre gondolkodnia kell. A hibás részösszeg sem szakítja meg a kosár további munkáit, ezért a jelentés a többi bemenetről is érdemi visszajelzést adhat.

A munkásszám ellenőrzése megelőzi a felosztást és a szálindítást. Hibás beállításnál így nem marad részben elindított folyamat, és a nulla osztó sem juthat el a maradékképzéshez. Ez ugyanaz a határelv, amelyet a típusos API-knál is használtunk: a használhatatlan állapotot minél korábban, egyértelmű eredménnyel utasítjuk el. A későbbi szálhibákat ettől külön kezeljük, mert azok már egy megkezdett futás lezárásakor jelennek meg.

Elfogadási feltételek

  1. Egytől négyig állítható munkásszám mellett minden bemeneti sorszám pontosan egyszer szerepel, rendezett kimenettel.
  2. A részösszeg hibája megmarad a megfelelő sorban, a végösszeg túlcsordulása külön eredmény.
  3. Az eredeti küldő lezárul, a fogadás és az összes join befejeződik, az üres bemenet is ellenőrzött.

Továbblépés

A szálak és join alapelvei itt teljes feldolgozási szerződéssé álltak össze. Bővítés előtt nevezd meg, mi változik: a bemenet tárolása, a munkakiosztás vagy a hibapolitika. Egyik új igény sem indokolja a teljességellenőrzés vagy a szálak rendezett lezárásának elhagyását.