Cél és előfeltétel
A scoped szeletfelosztás akkor kényelmes, ha minden szál külön részt kap. Egy közös számláló vagy eseménygyűjtemény más helyzet: több munkás ugyanazt az állapotot módosítja. Most két eszközt kapcsolunk össze. Az Arc megosztott tulajdont ad, a Mutex pedig kizárólagos hozzáférést szervez a belső értékhez. Egyik feladata sem helyettesíti a másikét.
A tulajdonmegosztás nem belső módosíthatóság
Az Arc::clone új tulajdonosi hivatkozást készít ugyanahhoz az allokációhoz. Nem másolja le a belső számlálót vagy vektort. Az atomikus referenciaszámlálás több szál közötti tulajdonmegosztást támogat, de a belső érték műveletei ettől nem válnak automatikusan biztonságossá. Az Arc<T> szálak közötti használhatósága a T megfelelő tulajdonságaitól is függ.
A Mutex::lock siker esetén őrt ad. Ezen keresztül érhető el a védett érték; az őr eldobásakor a zárolás feloldódik. A kritikus szakaszt ezért természetes blokkhatárral is rövidre lehet szabni. Hosszú számítás, külső művelet vagy másik szál befejezésére várakozás közben lehetőleg ne tartsuk a zárat. Különösen veszélyes lehet úgy join-olni, hogy a másik szálnak éppen ugyanarra a zárra lenne szüksége.
A zár kölcsönös kizárást biztosít, de nem választ helyettünk üzleti szabályt. Ha két mező együtt alkot érvényes állapotot, együtt kell őket ellenőrizni és frissíteni. Ha a módosítás közben korán visszatérünk, gondoljuk át, mely mezők változtak már meg. A szálbiztos hozzáférés és a logikailag érvényes állapot két eltérő követelmény, amelyeket ugyanabban a felületben kell összehangolni.
1. feladat: közös, lezárt számlálás
Négy szál mindegyike huszonöt alkalommal növeljen egy közös számlálót. Minden szálnak külön Arc tulajdonosi hivatkozást adj. A végső értéket csak az összes join után olvasd ki. Az ismert, kicsi műveletszám miatt a számláló nem csordulhat túl; az ellenőrzés a közös hozzáférés és a befejezés kapcsolatáról szól.
Megoldás
use std::{sync::{Arc, Mutex}, thread};
fn main() {
let counter = Arc::new(Mutex::new(0_u32));
let mut handles = Vec::new();
for _ in 0..4 {
let shared = Arc::clone(&counter);
handles.push(thread::spawn(move || {
for _ in 0..25 {
let mut guard = shared.lock().expect("mérgezett számláló");
*guard += 1;
}
}));
}
let joined: Vec<_> = handles.into_iter().map(|handle| handle.join()).collect();
for result in joined { result.unwrap(); }
assert_eq!(*counter.lock().unwrap(), 100);
}
Mérgezés: jelzés egy megszakadt módosításról
Ha egy szál a zárolást tartva pánikol, a mutex mérgezetté válhat. A következő lock ezt hibaként jelezheti, mert a védett állapot invariánsai sérülhettek. Ez figyelmeztető mechanizmus, nem általános bizonyíték minden lehetséges pánik észlelésére. A PoisonError belső értéke hozzáférhető, de a vak továbbhaladás üzleti szempontból nem feltétlenül helyes. A példákban a váratlan mérgezés hibát jelent, helyreállítást nem színlelünk.
2. feladat: események determinisztikus ellenőrzése
Három szál írja saját azonosítóját egy közös vektorba. A beérkezés sorrendjére nincs ígéret. A befejezés után készíts rendezett pillanatképet, és azt ellenőrizd. A rendezés itt azonosító szerinti megjelenítési döntés, nem a történések időrendjének rekonstruálása. A közös állapotból csak rövid ideig kölcsönzünk, utána a saját másolaton dolgozunk.
Megoldás
use std::{sync::{Arc, Mutex}, thread};
fn main() {
let events = Arc::new(Mutex::new(Vec::new()));
let handles: Vec<_> = (0..3).map(|id| {
let shared = Arc::clone(&events);
thread::spawn(move || shared.lock().unwrap().push((id, "kész")))
}).collect();
let joined: Vec<_> = handles.into_iter().map(|handle| handle.join()).collect();
for result in joined { result.unwrap(); }
let mut snapshot = { events.lock().unwrap().clone() };
snapshot.sort_unstable_by_key(|event| event.0);
assert_eq!(snapshot, vec![(0, "kész"), (1, "kész"), (2, "kész")]);
assert_eq!(events.lock().unwrap().len(), 3);
}
3. feladat: hibánál változatlan állapot
Készíts zárolt összegző műveletet, amely túlcsorduláskor visszatér anélkül, hogy módosítana. Először számold ki az új értéket, csak utána írd vissza. A mérgezést külön hibaként kezeld. Az állapotot minden hívás után ellenőrizd, így az elutasított művelet mellékhatása is vizsgálható. Ez egyetlen mutexen belüli állapotátmenet; több külön objektumot nem fog közös tranzakcióba.
Megoldás
use std::sync::Mutex;
fn add(total: &Mutex<u64>, value: u64) -> Result<(), &'static str> {
let mut guard = total.lock().map_err(|_| "mérgezett")?;
let next = guard.checked_add(value).ok_or("túlcsordulás")?;
*guard = next;
Ok(())
}
fn main() {
let total = Mutex::new(u64::MAX - 1);
assert_eq!(add(&total, 1), Ok(()));
assert_eq!(*total.lock().unwrap(), u64::MAX);
assert_eq!(add(&total, 1), Err("túlcsordulás"));
assert_eq!(*total.lock().unwrap(), u64::MAX);
assert_eq!(add(&total, 0), Ok(()));
}
A zár határának megválasztása
A közös vektor másolása a második feladatban tudatos egyszerűsítés: kis adatot vizsgálunk, és a rendezést kivisszük a kritikus szakaszból. Nagy állapotnál más pillanatkép-stratégia is szükséges lehet. A fontos kérdés az, mennyi munkához kell valóban kizárni másokat. Ne tedd a teljes feldolgozó folyamatot egyetlen zár mögé csak azért, mert így könnyű elérni a belső változókat.
A tesztek szándékosan nem követelik, hogy valamelyik munkás elsőként végezzen. A zárolás nem ígér a példák számára használható munkássorrendet vagy igazságos ütemezést. A helyes végállapotot és az elutasított módosítás változatlanságát viszont pontosan meg tudjuk fogalmazni. Ez erősebb, megismételhetőbb ellenőrzés, mint néhány jól sikerült kézi futás terminálüzeneteinek figyelése.
A zárolás sorrendjét érdemes egyszerűen tartani. Ezekben a feladatokban egy művelet egyetlen zárat fog meg, majd még más várakozás előtt elengedi. Ha később több mutexet vezetsz be, a különböző megszerzési sorrendek kölcsönös várakozást okozhatnak. Ezért már a felület tervezésekor keresd, mely adatok alkotnak egyetlen következetesen frissítendő egységet, és hol választható szét biztonságosan a munka.
Elfogadási feltételek
- A közös tulajdont Arc klónozása adja, az állapotváltozás kizárólag Mutex-őrön keresztül történik.
- Minden szál befejezése után ellenőrzünk, a vektor megjelenítési sorrendjét külön rendezzük.
- A túlcsordulás nem módosítja az állapotot, a mérgezés nincs elhallgatva vagy automatikusan sikernek tekintve.
Továbblépés
Néha egyszerűbb, ha egyetlen szál birtokolja az állapotot, a többi pedig adatot küld neki. A csatornák és tulajdonátadás ezt a szervezést mutatja meg. A közös zárak száma csökkenhet, de az életciklust és a leállást ott is meg kell tervezni.