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
- A scoped szálak helyi szeleteket kölcsönöznek, a hívó adata a visszatérés után tovább használható.
- A módosító feladat diszjunkt részeket ad át, az üres és egyelemű bemenet is ellenőrzött.
- 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.