Cél és előfeltétel

A határidő nem varázsolja félbe egy tetszőleges függvény végrehajtását. Ebben a leckében a feldolgozó maga ellenőrzi, hogy folytathatja-e a munkát. Előfeltétel a korlátos újrapróbálás és az explicit állapotgép. A példák egyetlen szálon, egész számlálóval szimulált idővel futnak; nem indítanak háttérmunkát és nem várnak valódi időre.

A kooperatív megszakítás azt jelenti, hogy a kód ismert ellenőrzési pontokon reagál egy kérésre. Két ilyen pont között a munka folytatódik. Egy lassú, blokkoló vagy ellenőrzés nélkül futó műveletet a puszta jelző nem szakít félbe. A megszakítási válaszidő ezért a munkadarabok hosszától és a ténylegesen megszakítható műveletektől is függ, nem csak egy bool létezésétől.

A leállítási ok és a haladás külön adat

A végrehajtás befejeződhet teljes sikerrel, lejárt határidővel vagy megszakítási kérés miatt. Az is fontos, mennyi munka történt már meg. Ha csak egyetlen Err eredményt adsz vissza, elveszhet a részleges haladás jelentése. A mai feladatok ezért a megfigyelt állapotot és a megállás okát együtt mutatják meg; ez nem jelenti a már elvégzett lépések visszavonását.

A határidő kizáró: amikor most egyenlő a határral, már nem kezdünk új munkát. A megszakítási kérésnek az első feladatban elsőbbséget adunk, ha mindkettő fennáll. Ez üzleti szerződés, nem a Rust automatikus szabálya. Ha más elsőbbséget szeretnél, azt a tesztben is nevezd meg; különben a felhasználó ugyanarra az állapotra véletlenszerűnek tűnő okot láthat.

1. feladat: fogalmazd meg az indulás feltételét

Írj kapufüggvényt most, határidő és megszakítási jelző alapján. A határ előtti időt fogadja el; az egyenlőség már lejárat legyen. Egyidejű lejárat és megszakítás esetén a megszakítást jelezze. A tesztek ne a gép órájától függjenek.

Megoldás
#[derive(Debug, PartialEq, Eq)]
enum Stop { Cancelled, Deadline }
fn allowed(now: u64, deadline: u64, cancelled: bool) -> Result<(), Stop> {
    if cancelled { return Err(Stop::Cancelled); }
    if now >= deadline { return Err(Stop::Deadline); }
    Ok(())
}
fn main() {
    assert_eq!(allowed(4, 5, false), Ok(()));
    assert_eq!(allowed(5, 5, false), Err(Stop::Deadline));
    assert_eq!(allowed(6, 5, false), Err(Stop::Deadline));
    assert_eq!(allowed(6, 5, true), Err(Stop::Cancelled));
}

A kapu egy döntési pillanatra vonatkozik. Az Ok nem jelent korlátlan jogosultságot a teljes hátralévő munkára. Egy később végrehajtott hosszú művelet alatt lejárhat az idő vagy érkezhet megszakítás. Ezért a kaput ott is újra kell értékelni, ahol a következő biztonságosan elhagyható munkadarab kezdődne.

2. feladat: látszódjon a két ellenőrzés közötti túlfutás

Egy munkadarabhoz szimulált költség tartozik. Csak a darabok határán ellenőrizd a határidőt, és add vissza az elvégzett darabok számát is. Három időegységes munkák ötegységes határnál két darabig juthatnak: a második már átlép a határon, mielőtt újra ellenőrizhetnénk.

Megoldás
#[derive(Debug, PartialEq, Eq)]
enum End { Complete, Deadline, ClockOverflow }
fn process(costs: &[u64], deadline: u64) -> (usize, End) {
    let mut now = 0_u64;
    let mut completed = 0;
    if now >= deadline { return (0, End::Deadline); }
    for cost in costs {
        if now >= deadline { return (completed, End::Deadline); }
        let Some(next) = now.checked_add(*cost) else {
            return (completed, End::ClockOverflow);
        };
        now = next;
        completed += 1;
    }
    if now >= deadline { (completed, End::Deadline) }
    else { (completed, End::Complete) }
}
fn main() {
    assert_eq!(process(&[3, 3, 3], 5), (2, End::Deadline));
    assert_eq!(process(&[2, 2], 5), (2, End::Complete));
    assert_eq!(process(&[], 5), (0, End::Complete));
    assert_eq!(process(&[1], 0), (0, End::Deadline));
    assert_eq!(process(&[1, u64::MAX], u64::MAX), (1, End::ClockOverflow));
}

A modell nem állítja, hogy pontosan a határidőben megáll. Éppen azt mutatja meg, miért számít a munkadarab mérete. Az utolsó darab után is vizsgáljuk a határt, ezért egy teljes adatsort feldolgozó futás sem kap automatikusan időben teljesült minősítést. A completed azt számolja, ami a modell szerint már elvégzett munka, nem azt, amit szükség esetén vissza tudnánk vonni.

3. feladat: állj meg a következő együttműködési ponton

Készíts olyan feldolgozót, amely minden elem előtt meghív egy megszakítást jelző függvényt. A tesztben a második index elérésekor kérj leállást. Az első két eredmény maradjon látható, további ne keletkezzen. A Cell itt kizárólag egy szálon működő tesztállapot, nem szálak közötti megszakítási protokoll.

Megoldás
use std::cell::Cell;
fn collect_until(n: usize, mut cancelled: impl FnMut(usize) -> bool) -> Vec<usize> {
    let mut completed = Vec::new();
    for index in 0..n {
        if cancelled(index) { break; }
        completed.push(index);
    }
    completed
}
fn main() {
    let requested = Cell::new(false);
    let result = collect_until(5, |index| {
        if index == 2 { requested.set(true); }
        requested.get()
    });
    assert_eq!(result, vec![0, 1]);
    assert!(collect_until(5, |_| true).is_empty());
    assert_eq!(collect_until(3, |_| false), vec![0, 1, 2]);
}

A már elkészült elemeket nem töröljük azért, mert később megszakítás érkezett. Valódi mellékhatásnál külön kell eldönteni, mi számít befejezettnek, mi folytatható és mi igényel kompenzációt. A több szál közötti jelzéshez megfelelő szinkronizáció és tulajdonosi megállapodás kellene; a helyi Cell használatát erre nem lehet változatlanul átmásolni.

Hol legyen az utolsó ellenőrzés?

Egy visszafordíthatatlan művelet előtt érdemes újra megvizsgálni a feltételeket, de az ellenőrzés és a külső végrehajtás közötti rés ettől még létezhet. Az állapotgépnek ezért meg kell különböztetnie a még el nem indított és a már bizonytalan kimenetű munkát. A megszakítási kérés önmagában nem bizonyítja, hogy a külvilágban semmi nem történt.

A függvények a tesztben pontosan ismert időadatot kapnak. Egy valós alkalmazásban monoton időmegfigyelésre alkalmas órát kellene a kapuhoz kapcsolni, nem naptári dátumot összeadogatni. A mostani determinisztikus modell az egyenlőség, elsőbbség és részeredmény szerződését ellenőrzi, nem operációsrendszer-szintű határidő-garanciát. Ezért nincs sleep vagy időtartamra tett teljesítményállítás.

A megszakítás után megmaradó részhaladást a hívó felületnek is értelmeznie kell. A felhasználó számára más döntés következik a nulla elvégzett lépésből és a már befejezett első két elemből. A leállási ok önmagában kevés, ha az alkalmazás később folytatást vagy megismétlést kínál ugyanahhoz a munkához.

Elfogadási feltételek

  1. Pontosan a határidőn új munka nem indul, egyidejű megszakítás a rögzített elsőbbséget követi.
  2. A darabos feldolgozás megőrzi az elvégzett munka számát, és a túlfutást nem nevezi időben teljesült sikernek.
  3. A második indexnél kért megszakítás után az eredmény pontosan [0, 1], további elem nélkül.

Összefoglaló és továbblépés

A következő napló és visszajátszás leckében már azt is megnevezzük, milyen állapotból lehetne helyreállni. A megszakításra válaszoló memóriabeli ciklus önmagában nem tartós helyreállítás.