Cél és előfeltétel

A szálindítás és join leckében a munkaszál saját adatot kapott. Ez egyszerű életciklust ad, de néha a bemenet az indító függvény helyi vektorában maradna. A thread::scope olyan határt hoz létre, amelyből a szálak nem élhetnek tovább. Emiatt megfelelő feltételekkel helyi adatot is kölcsönözhetnek. Most változatlan szeletek olvasását, diszjunkt szeletek módosítását és a részeredmények visszaadását gyakoroljuk.

A hatókör befejezése tartja életben a kölcsönzést

A scoped szálak a hatókör lezárása előtt befejeződnek. Az indító függvény ezért nem engedheti el alattuk a kölcsönzött helyi adatot. Ez a befejezési garancia teszi lehetővé a rövidebb életű hivatkozásokat. Nem kapcsolja ki a Rust szokásos kölcsönzési szabályait: több megosztott olvasás megengedett, de ugyanarra az adatra egymással átfedő módosító hozzáférés továbbra sem adható ki.

A szálak indítása önmagában nem bizonyítja, hogy érdemes párhuzamosítani. A feladat mérete és a szervezési költség számít. A leckében a felosztás átláthatóságát vizsgáljuk. A kis tömbök nem sebességmérések, hanem olyan tesztadatok, amelyek mellett pontosan ellenőrizhető, melyik rész hová kerül. Későbbi alkalmazásban először helyes felosztást, azután méréssel igazolt munkaméretet keress.

A scoped szál pánikja sem lesz automatikusan saját Result hibává. Ha egy automatikusan bevárt szál pánikol, a scope is pánikolhat. A visszakapott scoped handle join hívásával külön vizsgálható a befejezés. Példáink ismert, korlátozott értékeken dolgoznak, és a váratlan pánikot tesztkudarcnak tekintik. Várt bemeneti hibára továbbra is normál Result illik.

1. feladat: két kölcsönzött félösszeg

Oszd a bemenetet két részre split_at segítségével, majd mindkét rész összegét külön szál számolja. A függvény csak szeletet kapjon, és a hívó a visszatérés után továbbra is használhassa a vektorát. Az üres és a páratlan hosszúságú adatnál is pontosan egyszer szerepeljen minden elem. Az összegzés a kis tesztértékek tartományában marad.

Megoldás
use std::thread;
fn sums(values: &[u32]) -> (u32, u32) {
    let (left, right) = values.split_at(values.len() / 2);
    thread::scope(|scope| {
        let a = scope.spawn(|| left.iter().sum());
        let b = scope.spawn(|| right.iter().sum());
        (a.join().unwrap(), b.join().unwrap())
    })
}
fn main() {
    let values = vec![1, 2, 3, 4, 5];
    assert_eq!(sums(&values), (3, 12));
    assert_eq!(values.len(), 5);
    assert_eq!(sums(&[]), (0, 0));
    assert_eq!(sums(&[7]), (0, 7));
}

Miért fontos a felosztó művelet?

A split_at_mut két egymást nem fedő módosítható szeletet ad vissza. Ezt a tulajdonságot a standard könyvtár szerződése biztosítja. Nem kézzel próbálunk ugyanabból a vektorból két tetszőleges indexelt kölcsönzést kicsikarni. Az adatok határa így a típusokban is látszik: a bal szál nem tudja elérni a jobb rész elemeit a kapott szeletén keresztül.

2. feladat: diszjunkt módosítás

A bal fél minden elemét növeld eggyel, a jobb fél elemeit kettővel. A tesztadatokon nem lehetséges túlcsordulás. A scope lezárása után a teljes szeletet ellenőrizd, mert csak ebből látszik egyszerre a választóvonal és a módosítás iránya. Egyelemű bemenetnél a bal fél üres; az egyetlen elem a jobb szabályt követi.

Megoldás
use std::thread;
fn update(values: &mut [i32]) {
    let middle = values.len() / 2;
    let (left, right) = values.split_at_mut(middle);
    thread::scope(|scope| {
        scope.spawn(|| { for n in left { *n += 1; } });
        scope.spawn(|| { for n in right { *n += 2; } });
    });
}
fn main() {
    let mut values = [0, 10, 20, 30];
    update(&mut values);
    assert_eq!(values, [1, 11, 22, 32]);
    let mut one = [5];
    update(&mut one);
    assert_eq!(one, [7]);
    update(&mut []);
}

3. feladat: eredeti sorrendű részeredmények

A két fél ne egyszerű összeget, hanem a benne lévő pozitív számok új vektorát adja vissza. A részeket balról jobbra fűzd össze. A szálak befejezési sorrendje nem változtathatja meg a bemeneti sorrendet. Az eredeti szeletet csak olvassuk, a létrehozott eredmény viszont saját vektor, ezért a kölcsönzés lezárása után is használható.

Megoldás
use std::thread;
fn positive(values: &[i32]) -> Vec<i32> {
    let (left, right) = values.split_at(values.len() / 2);
    thread::scope(|scope| {
        let a = scope.spawn(|| left.iter().copied().filter(|n| *n > 0).collect::<Vec<_>>());
        let b = scope.spawn(|| right.iter().copied().filter(|n| *n > 0).collect::<Vec<_>>());
        let mut first = a.join().unwrap();
        let second = b.join().unwrap();
        first.extend(second);
        first
    })
}
fn main() {
    assert_eq!(positive(&[3, -1, 0, 2, 7]), vec![3, 2, 7]);
    assert!(positive(&[-2, 0]).is_empty());
    assert!(positive(&[]).is_empty());
}

Határválasztás és tesztelés

Az üres fél nem rendkívüli eset, hanem a felosztás természetes következménye. Ha a logikád eleve támogatja, kevesebb külön ág szükséges. Páratlan hosszúságnál viszont legyen egyértelmű, melyik fél kap több elemet. Itt az egész osztás miatt a jobb rész hosszabb. A sorrendet megőrző feladatban ennek nincs tartalmi hatása, a módosító feladatban viszont befolyásolja, melyik szabály érinti a középső elemet.

A kölcsönzés visszaadása nem azonos az adatok visszavonásával. Ha egy szál már módosított egy elemet, majd később valami hibát jelez, a scope nem gyárt automatikus tranzakciót. Ha ilyen szerződés szükséges, előbb készíts külön eredményeket, és csak sikeres ellenőrzés után cseréld le a teljes állapotot. A harmadik feladat elkülönített eredményvektorai ennek a gondolkodásnak egyszerű építőelemei.

A move és a kölcsönzés itt sem ellentétes fogalom. Egy lezárás magába mozgathat egy hivatkozást, miközben az eredeti adat tulajdona továbbra is a hívónál marad. A lényeg az átadott érték típusa: vektor mozgatásakor tulajdon kerül át, szeletre mutató hivatkozás mozgatásakor a kölcsönzés kerül a szál környezetébe. Ezt az aláírásból olvasd ki, ne pusztán a kulcsszóból.

Elfogadási feltételek

  1. A scoped szálak helyi szeleteket kölcsönöznek, a hívó adata a visszatérés után tovább használható.
  2. A módosító feladat diszjunkt részeket ad át, az üres és egyelemű bemenet is ellenőrzött.
  3. A szűrt eredmény az eredeti sorrendet követi, függetlenül a szálak befejezési sorrendjétől.

Továbblépés

Amikor az adatok nem oszthatók ilyen tisztán külön részekre, más eszköz kell. Az Arc és Mutex leckében közös állapotot kezelünk. A kölcsönzés, a tulajdon megosztása és a módosítás összehangolása ott is külön döntés marad.