Pravidla plánování úloh je možno použít k řízení, když se vaše úlohy spouštějí ve vztahu k jiným úlohám. Pravidla plánování umožňují zejména zabránit tomu, aby několik úloh běželo současně v situacích, kdy souběžnost může vést k nekonzistentním výsledkům. Také umožňují zaručit pořadí provádění série úloh. Schopnosti pravidel plánování nejlépe ilustruje následující příklad. Začněme tím, že si nadefinujeme dvě úlohy, které slouží k souběžnému rozsvěcení a zhasínání světla:
public class LightSwitch { private boolean isOn = false; public boolean isOn() { return isOn; } public void on() { new LightOn().schedule(); } public void off() { new LightOff().schedule(); } class LightOn extends Job { public LightOn() { super("Rozsvícení světla"); } public IStatus run(IProgressMonitor monitor) { System.out.println("Rozsvícení světla"); isOn = true; return Status.OK_STATUS; } } class LightOff extends Job { public LightOff() { super("Zhasnutí světla"); } public IStatus run(IProgressMonitor monitor) { System.out.println("Zhasnutí světla"); isOn = false; return Status.OK_STATUS; } } }
Nyní můžeme vytvořit jednoduchý program, který vytvoří vypínač světla a nejprve jej zapne a potom opět vypne:
LightSwitch light = new LightSwitch(); light.on(); light.off(); System.out.println("Je světlo rozsvícené?" + switch.isOn());
Pokud tento malý program zopakujeme vícekrát, obdržíme nakonec následující výstup:
Zhasnutí světla Rozsvícení světla Je světlo rozsvícené? true
Jak je to možné? Dali jsme přece příkaz k rozsvícení světla a poté k jeho zhasnutí, takže jeho výsledný stav by měl být zhasnuto! Problém je v tom, že nic nebránilo úloze LightOff (zhasnout), aby neprobíhala současně s úlohou LightOn (rozsvítit). A tak i přesto, že úloha "rozsvítit" byla naplánována jako první, jejich souběžné provedení znamená, že nelze žádným způsobem předpovědět přesné pořadí provedení dvou souběžných úloh. Pokud úloha LightOff skončí před úlohou LightOn, dostaneme tento neplatný výsledek. Potřebujeme tedy něco, co těmto dvěma úlohám zabrání probíhat souběžně, a právě zde se uplatní pravidla plánování.
Problém uvedený v tomto příkladu můžeme vyřešit vytvořením jednoduchého pravidla plánování, které působí jako mutex (také známý pod názvem binární semafor):
class Mutex implements ISchedulingRule { public boolean isConflicting(ISchedulingRule rule) { return rule == this; } public boolean contains(ISchedulingRule rule) { return rule == this; } }
Toto pravidlo poté přidáme ke dvěma úlohám pro ovládání světla z předchozího příkladu:
public class LightSwitch { final MutextRule rule = new MutexRule(); ... class LightOn extends Job { public LightOn() { super("Rozsvícení světla"); setRule(rule); } ... } class LightOff extends Job { public LightOff() { super("Zhasnutí světla"); setRule(rule); } ... } }
Nyní při plánování těchto dvou úloh pro ovládání světla infrastruktura úloh vyvolá metodu isConflicting k porovnání pravidel plánování těchto dvou úloh. Tato metoda zjistí, že tyto dvě úlohy mají pravidla plánování ve vzájemném nesouladu a zajistí jejich spuštění ve správném pořadí. Také zajistí, aby nikdy neprobíhaly současně. Nyní i kdybyste program uvedený v příkladu spustili milionkrát, vždy dostanete tentýž výsledek:
Rozsvícení světla Zhasnutí světla Je světlo rozsvícené? false
Pravidla lze používat i nezávisle na úlohách jako obecný uzamykací mechanizmus. Následující příklad slouží k získání pravidla v rámci bloku try/finally, který zabraňuje ve spouštění ostatních vláken (procesů) a úloh prostřednictvím pravidla stanovujícího dobu, která musí uplynout mezi vyvoláním pravidel beginRule a endRule.
IJobManager manager = Platform.getJobManager(); try { manager.beginRule(rule, monitor); ... do some work ... } finally { manager.endRule(rule); }
Při získávání a uvolňování pravidel plánování s takovýmto vzorkem kódování byste měli postupovat velmi obezřetně. Pokud neukončíte pravidlo nazvané beginRule, nastálo jej zablokujete.
Přestože API úloh definuje kontrakt pravidel plánování, ve skutečnosti neposkytuje žádné implementace pravidel plánování. Generická infrastruktura v podstatě nemá žádnou možnost, jak zjistit, které sady úloh se mohou bez problémů spouštět souběžně. Ve výchozím nastavení nemají úlohy žádná pravidla plánování a naplánovaná úloha se provede tak rychle, jak rychle se podaří vytvořit vlákno (proces) pro její provedení.
Pokud má úloha přece jen pravidlo plánování, pomocí metody isConflicting se zjistí, zda toto pravidlo není v rozporu s pravidly kterýchkoli úloh, jež jsou právě spuštěny. Vaše implementace metody isConflicting tak může přesně definovat, kdy je bezpečné provést vaši úlohu. V našem příkladu s vypínačem světla implementace metody isConflicting jednoduše používá porovnání identity s poskytnutým pravidlem. Má-li jiná úloha identické pravidlo, nebudou tyto úlohy spouštěny souběžně. Při psaní vlastních pravidel plánování si pozorně prostudujte kontrakt API pro metodu isConflicting a dodržujte jej.
Pokud má vaše úloha několik navzájem nesouvisejících omezení, můžete sestavit několik pravidel plánování dohromady za použití metody MultiRule. Například pokud vaše úloha potřebuje rozsvítit světlo a zároveň zapsat informace do síťové zásuvky, může mít pravidlo pro vypínač světla a pravidlo pro přístup umožňující zápis do zásuvky, jež lze zkombinovat do jediného pravidla za pomoci tovární metody MultiRule.combine.
Již jsme probrali metodu isConflicting použitou na pravidlo ISchedulingRule, ale zatím jsme se nezmínili o metodě contains. Tato metoda se používá pro značně specializované uplatnění pravidel plánování, které nebude požadovat mnoho klientů. Pravidla plánování lze logicky sestavit do hierarchie pro řízení přístupu k prostředkům s přirozeným hierarchickým uspořádáním. Nejjednodušším příkladem pro ilustraci tohoto konceptu je systém souborů na bázi stromu. Pokud chce nějaká aplikace získat výlučný zámek pro určitý adresář, obvykle se předpokládá, že chce také výlučný přístup k souborům a podadresářům uvnitř takového adresáře. K určení hierarchických vztahů mezi jednotlivými zámky slouží metoda contains. Pokud nepotřebujete vytvářet hierarchii zámků, můžete implementovat metodu contains pouze k vyvolání metody isConflicting.
Následující příklad ukazuje hierarchický zámek pro řízení přístupu umožňujícího zápis do popisovačůjava.io.File.
public class FileLock implements ISchedulingRule { private String path; public FileLock(java.io.File file) { this.path = file.getAbsolutePath(); } public boolean contains(ISchedulingRule rule) { if (this == rule) return true; if (rule instanceof FileLock) return path.startsWith(((FileLock) rule).path); if (rule instanceof MultiRule) { MultiRule multi = (MultiRule) rule; ISchedulingRule[] children = multi.getChildren(); for (int i = 0; i < children.length; i++) if (!contains(children[i])) return false; return true; } return false; } public boolean isConflicting(ISchedulingRule rule) { if (!(rule instanceof FileLock)) return false; String otherPath = ((FileLock)rule).path; return path.startsWith(otherPath) || otherPath.startsWith(path); } }
Metoda contains se uplatní, pokud se vlákno snaží získat druhé pravidlo, ačkoli už jedno pravidlo má. Aby se zabránilo možnosti vzniku zablokování, může jakékoli dané vlákno mít v každém okamžiku pouze jedno pravidlo plánování. Pokud vlákno vyvolá pravidlo beginRule, ačkoli už jedno pravidlo má, a to buď prostřednictvím předchozího vyvolání pravidla beginRule, nebo provedením úlohy s pravidlem plánování, použije se metoda contains, aby se zjistilo, zda jsou tato dvě pravidla ekvivalentní. Pokud metoda contains pro již vlastněné pravidlo vrátí hodnotu true, vyvolání pravidla beginRule bude úspěšné. Pokud metoda contains vrátí hodnotu false, dojde k výskytu chyby.
Abychom si to vysvětlili konkrétněji, řekněme, že určité vlákno (proces) vlastní pravidlo FileLock z našeho příkladu, které se vztahuje na určitý adresář v adresáři "c:\temp". Dokud vlastní toto pravidlo, smí modifikovat soubory pouze v rámci podstromu tohoto adresáře. Pokud se pokusí modifikovat soubory v jiných adresářích, které nepatří do adresáře "c:\temp", nemělo by se mu to podařit. Pravidlo plánování je konkrétní specifikací toho, co určité vlákno (proces) nebo úloha smí či nesmí dělat. Porušení této specifikace způsobí běhovou výjimku. V literatuře věnované souběžnosti se této technice říká dvoufázové zamykání. Ve schématu dvoufázového zamykání musí proces předem určit všechny zámky, které bude potřebovat pro určitou úlohu, a během operace potom již nesmí získat další zámky. Dvoufázové zamykání eliminuje podmínku hold-and-wait, která je nezbytným předpokladem pro vznik zablokování cyklickým čekáním. Proto se systém, který využívá pouze pravidla plánování jako primitivní způsob zamykání, nemůže zablokovat.