Le regole di pianificazione dei lavori possono essere utilizzate per controllare quando i lavori vengono eseguiti in relazione ad altri lavori. In particolare, le regole di pianificazione consentono di evitare che più lavori vengano eseguiti contemporaneamente in situazioni in cui la simultaneità può portare a risultati incoerenti. Consentono inoltre di garantire l'ordine di esecuzione di una serie di lavori. L'importanza delle regole di pianificazione è illustrata nei dettagli mediante un esempio. Vengono definiti due lavori, utilizzati per accendere e spegnere due interruttori contemporaneamente:
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("Turning on the light"); } public IStatus run(IProgressMonitor monitor) { System.out.println("Turning the light on"); isOn = true; return Status.OK_STATUS; } } class LightOff extends Job { public LightOff() { super("Turning off the light"); } public IStatus run(IProgressMonitor monitor) { System.out.println("Turning the light off"); isOn = false; return Status.OK_STATUS; } } }
Viene creato un programma semplice che crea un interruttore e lo accende e spegne:
LightSwitch light = new LightSwitch(); light.on(); light.off(); System.out.println("The light is on? " + switch.isOn());
Se questo programma viene eseguito un numero sufficiente di volte, viene visualizzato l'output di seguito riportato:
Turning the light off Turning the light on The light is on? true
Qual è il motivo? La luce è stata accesa e spenta, quindi lo stato finale dovrebbe essere spento. Il problema è che non esiste alcun elemento che impedisca l'esecuzione del lavoro LightOff contemporaneamente al lavoro LightOn. Quindi, anche se il lavoro "on" è stato pianificato per primo, l'esecuzione contemporanea indica che non esiste alcun modo per prevedere l'ordine esatto di esecuzione dei due lavori contemporanei. Se l'esecuzione del lavoro LightOff termina prima del lavoro LightOn, viene visualizzato un risultato non valido. È necessario quindi evitare che i due lavori vengano eseguiti contemporaneamente ed a tal scopo vengono utilizzate le regole di pianificazione.
È possibile correggere questo esempio creando una semplice regola di pianificazione che agisca come mutex (conosciuto anche come semaforo binario):
class Mutex implements ISchedulingRule { public boolean isConflicting(ISchedulingRule rule) { return rule == this; } public boolean contains(ISchedulingRule rule) { return rule == this; } }
Questa regola viene quindi aggiunta ai lavori degli interruttori dell'esempio precedente:
public class LightSwitch { final MutextRule rule = new MutexRule(); ... class LightOn extends Job { public LightOn() { super("Turning on the light"); setRule(rule); } ... } class LightOff extends Job { public LightOff() { super("Turning off the light"); setRule(rule); } ... } }
Ora, quando i lavori dei due interruttori vengono pianificati, l'infrastruttura dei lavori richiama il metodo isConflicting per il confronto delle regole di pianificazione dei due lavori. I due lavori presentano regole di pianificazione in conflitto ed è necessario verificare che vengano eseguiti nell'ordine corretto. È necessario inoltre verificare che i lavori non vengano mai eseguiti contemporaneamente. Se si esegue il programma di esempio un milione di volte, viene sempre visualizzato lo stesso risultato:
Turning the light on Turning the light off The light is on? false
Le regole possono anche essere utilizzate indipendentemente dai lavori, come un meccanismo di blocco generale. L'esempio di seguito riportato acquisisce una regola all'interno di un blocco try/finally, impedendo ad altri thread e ad altri lavori l'esecuzione con quella regola per la durata compresa tra i richiami di beginRule e endRule.
IJobManager manager = Platform.getJobManager(); try { manager.beginRule(rule, monitor); ... do some work ... } finally { manager.endRule(rule); }
È necessario esercitare un'estrema cautela quando si acquisiscono e si rilasciano le regole di pianificazione utilizzando questo modello di codifica. Se non si riesce a terminare una regola beginRule, questa regola viene bloccata per sempre.
Nonostante l'API del lavoro definisca il contratto delle regole di pianificazione, non fornisce alcuna implementazione delle regole di pianificazione. L'infrastruttura generica non ha alcun modo di individuare gli insiemi di lavoro che possono essere eseguiti contemporaneamente. Per impostazione predefinita, i lavori non presentano regole di pianificazione ed un lavoro pianificato viene eseguito con la stessa rapidità con cui un thread può essere creato per eseguire il lavoro.
Quando un lavoro non presenta una regola di pianificazione, viene utilizzato il metodo isConflicting, per determinare se la regola è in conflitto con le regole degli altri lavori in esecuzione contemporaneamente. Quindi, l'implementazione di isConflicting può definire esattamente quando un lavoro può essere eseguito. Nell'esempio degli interruttori, l'implementazione di isConflicting utilizza semplicemente un confronto di identità con la regola fornita. Se un altro lavoro presenta una regola identica, i lavori non verranno eseguiti contemporaneamente. Quando si scrivono le regole di pianificazione personalizzate, è necessario leggere e seguire attentamente il contratto API relativo a isConflicting.
Se il lavoro presenta varie limitazioni non correlate, è possibile creare più regole di pianificazione insieme, utilizzando una MultiRule. Ad esempio, se il lavoro deve accendere un interruttore e scrivere le informazioni nel socket di una rete, può presentare una regola per l'interruttore e una regola per l'accesso in scrittura al socket combinate in un'unica regola, utilizzando il metodo factory MultiRule.combine.
È stato illustrato il metodo isConflicting in ISchedulingRule, ma non è stato menzionato il metodo contains. Questo metodo viene utilizzato per un'applicazione specifica delle regole di pianificazione che molti client non richiedono. Le regole di pianificazione possono essere composte in modo logico in gerarchie, per il controllo dell'accesso a risorse gerarchiche. L'esempio più semplice per illustrare questo concetto è un file system basato su una struttura ad albero. Se l'applicazione desidera acquisire un blocco esclusivo su una directory, ciò in genere implica che l'applicazione desidera anche l'accesso esclusivo ai file e alle sottodirectory all'interno di quella directory. Il metodo contains viene utilizzato per specificare la relazione gerarchica tra i blocchi. Se non è necessario creare gerarchie di blocchi, è possibile implementare il metodo contains per richiamare semplicemente isConflicting.
Di seguito viene riportato un esempio di blocco gerarchico per il controllo dell'accesso in scrittura ai gestori 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); } }
Il metodo contains viene utilizzato se un thread tenta di acquisire una seconda regola quando già dispone di una regola. Per evitare la possibilità di un deadlock, i thread possono disporre di un'unica regola alla volta. Se un thread richiama una regola beginRule quando già dispone di una regola, mediante una chiamata precedente a beginRule o eseguendo un lavoro con una regola di pianificazione, viene consultato il metodo contains, per verificare se le due regole sono equivalenti. Se il metodo contains relativo a una regola già esistente restituisce true, la chiamata di beginRule avrà un risultato positivo. Se il metodo contains restituisce false, si verifica un errore.
In termini concreti, un thread dispone della regola di esempio FileLock nella directory "c:\temp". Il thread può quindi solo modificare i file all'interno della struttura secondaria della directory. Se tenta di modificare i file contenuti in altre directory diverse da "c:\temp", la sua esecuzione non riesce. Una regola di pianificazione è quindi una definizione concreta di ciò che un lavoro o un thread può eseguire o meno. Se si viola questa definizione, viene visualizzata un'eccezione di runtime. Attualmente, questa tecnica è nota come blocco a due fasi. In uno schema di blocco a due fasi, un processo deve specificare in anticipo tutti i blocchi necessari per una determinata attività e non può acquisire altri blocchi durante il funzionamento. Il blocco a due fasi elimina la condizione hold-and-wait che rappresenta un prerequisito per un deadlock di attesa circolare. Quindi, è impossibile per un sistema che utilizza le regole di pianificazione solo come un blocco primitivo immettere un deadlock.