Regeln der Zeitplanung für Jobs können verwendet werden, um zu steuern, wann Ihre Jobs im Verhältnis zu anderen Jobs ausgeführt werden sollen. Vor allem können Sie in Situationen, in denen ein gemeinsamer Zugriff zu inkonsistenten Ergebnissen führen würde, über Regeln der Zeitplanung verhindern, dass mehrere Jobs gleichzeitig ausgeführt werden. Außerdem garantieren Sie die Ausführungsreihenfolge einer Reihe von Jobs. Dies wird am besten anhand eines Beispiels erläutert. Zunächst werden zwei Jobs definiert, die gleichzeitig ein Licht an- und ausschalten sollen.
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; } } }
Nun wird ein einfaches Programm erstellt, dass einen Lichtschalter erstellt, und diesen an- und wieder ausschaltet:
LightSwitch light = new LightSwitch(); light.on(); light.off(); System.out.println("The light is on? " + switch.isOn());
Wenn dieses kleine Programm oft genug ausgeführt wird, so erscheint früher oder später die folgende Ausgabe:
Turning the light off Turning the light on The light is on? true
Wie kann das sein? Das Licht sollte doch erst an und dann wieder ausgeschaltet werden, daher müsste eigentlich der abschließende Status 'aus' sein. Das Problem liegt darin, dass nicht verhindert wird, dass der Job LightOff gleichzeitig mit dem Job LightOn ausgeführt wird. Dies bedeutet, dass es aufgrund der gleichzeitigen Ausführung unmöglich ist, die Ausführungsreihenfolge der beiden gleichzeitigen Jobs vorherzusagen, selbst wenn der Job "an" als erster terminiert wurde. Wenn der Job LightOff vor dem Job LightOn läuft, so erhalten wir dieses ungültige Ergebnis. Es muss also verhindert werden, dass die beiden Jobs gleichzeitig ausgeführt werden. An dieser Stellen helfen Regeln für die Zeitplanung weiter.
Das Beispiel kann durch die Erstellung einer einfachen Terminierungsregeln, die als mutex fungiert (auch als binäres Semaphor bezeichnet), repariert werden:
class Mutex implements ISchedulingRule { public boolean isConflicting(ISchedulingRule rule) { return rule == this; } public boolean contains(ISchedulingRule rule) { return rule == this; } }
Diese Regel wird dann zu den beiden Lichtschalter-Jobs aus dem vorherigen Beispiel hinzugefügt:
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); } ... } }
Wenn jetzt die beiden Lichtschalter-Jobs terminiert werden, ruft die Job-Infrastruktur die Methode isConflicting auf, um die Regeln für die Zeitplanung der beiden Jobs zu vergleichen. Die Methode stellt fest, dass die Regeln der Zeitplanung der beiden Jobs im Konflikt zu einander stehen und stellt sicher, dass sie in der richtigen Reihenfolge ausgeführt werden. Es garantiert außerdem, dass sie niemals gleichzeitig ausgeführt werden. Jetzt können Sie das Beispielprogramm auch eine Million Mal ausführen, Sie werden immer das gleiche Ergebnis erhalten:
Turning the light on Turning the light off The light is on? false
Regeln können auch unabhängig von Jobs als Sperrmechanismus verwendet werden. Das folgende Beispiel erhält innerhalb eines Blocks 'try/finally' eine Regel, die verhindert, dass andere Threads und Jobs im Zeitraum zwischen den beiden Aufrufen beginRule und endRule mit dieser Regel ausgeführt werden.
IJobManager manager = Platform.getJobManager(); try { manager.beginRule(rule, monitor); ... do some work ... } finally { manager.endRule(rule); }
Sie sollten sehr vorsichtig sein, wenn Sie Regeln der Zeitplanung über ein solches Codierungsmuster übernehmen und freigeben. Wenn Sie eine Regel, die Sie mit beginRule aufgerufen haben, nicht beenden, so ist diese Regel für immer gesperrt.
Obwohl die Job-API den Vertrag der Zeitplanungsregeln definiert, bietet sie keinerlei Implementierung für Zeitplanungsregeln. Die generische Infrastruktur hat keine Möglichkeit um herauszufinden, welche Gruppen von Jobs gleichzeitig ausgeführt werden dürfen. Jobs haben standardmäßig keine Zeitplanungsregeln, und ein terminierter Job wird ausgeführt, sobald ein Thread für seine Ausführung erstellt werden kann.
Wenn ein Job über eine Zeitplanungsregel verfügt, wird die Methode isConflicting verwendet, um zu bestimmen, ob die Regel mit den Regeln andere zur Zeit laufenden Jobs im Konflikt steht. Auf diese Art kann Ihre Implementierung von isConflicting genau definieren, wann es in Ordnung ist, Ihren Job auszuführen. Im Lichtschalterbeispiel verwendet die Implementierung isConflicting einfach einen Identitätsvergleich mit der der bereitgestellten Regel. Wenn ein anderer Job über dieselbe Regel verfügt, werden sie nicht gleichzeitig ausgeführt. Wenn Sie eigene Zeitplanungsregeln schreiben, sollten Sie den API-Vertrag für isConflicting sehr genau durchlesen und befolgen.
Wenn in Ihrem Job mehrere Einschränkung enthalten sind, die nicht zusammenhängen, können Sie über MultiRule auch mehrere Zeitplanungsregeln erstellen. Wenn Ihr Job beispielsweise einen Lichtschalter anschalten und gleichzeitig Informationen in ein Netzwerksocket eintragen soll, kann er eine Regel für den Lichtschalter und eine andere für den Schreibzugriff auf den Socket haben. Über die Factory-Methode MultiRule.combine können diese beiden Regeln zu einer einzigen zusammengefasst werden.
Die Methode isConflicting wurde bereits unter ISchedulingRule behandelt, aber die Methode contains wurde bisher noch nicht erwähnt. Diese Methode wird für eine ziemlich spezielle Anwendung von Zeitplanungsregeln verwendet, die nur wenige Clients benötigen. Um Zugriff auf Ressourcen zu steuern, die von Natur aus hierarchisch aufgebaut sind, können Zeitplanungsregeln logisch in Hierarchien gegliedert werden. Das leichteste Beispiel, um diese darzustellen, ist das Konzept eines Dateisystem mit einer Baumstruktur. Wenn eine Anwendung ein Verzeichnis exklusiv sperrt, so impliziert dies normalerweise, dass es auch exklusiv auf die Dateien und Unterverzeichnisse dieses Verzeichnisses zugreifen will. Die Methode contains wird verwendet, um die hierarchischen Beziehungen zwischen Sperren zu bestimmen. Wenn Sie keine Hierarchie für Sperren erstellen wollen, können die Methode contains einfach so implementieren, dass sie isConflicting aufruft.
Das folgende Beispiel ist eine hierarchische Sperre für den Schreibzugriff auf Kennungen des Typs 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); } }
Auf die Methode contains wird zugegriffen, wenn ein Thread, das bereits über eine Regel verfügt, versucht, eine zweite Regel zu erwerben. Um die Gefahr einer gegenseitigen Sperre zu vermeiden, kann jeder Thread zu jeder Zeit nur über eine Zeitplanungsregel verfügen. Wenn ein Thread, das bereits über eine Regel verfügt, beginRule aufruft (entweder durch einen früheren Aufruf von beginRule oder durch die Ausführung eines Jobs mit einer Zeitplanungsregel), wird die Methode contains hinzugezogen, um zu untersuchen, ob die beiden Regeln äquivalent sind. Wenn die Methode contains für die bereits aktive Regel den Wert true zurückgibt, hat der Aufruf beginRule Erfolg. Gibt die Methode contains den Wert false zurück, so tritt ein Fehler auf.
Konkreter gesagt: angenommen, im Beispiel ist ein Thread Eigner der Regel FileLock für das Verzeichnis "c:\temp". Solange es Eigner dieser Regel ist, kann es nur Dateien innerhalb der Unterverzeichnisstruktur dieses Verzeichnisses ändern. Sollte es versuchen, Dateien in anderen Verzeichnissen zu ändern, die sich nicht unter "c:\temp" befinden, sollte dieser Versuch fehlschlagen. Eine Zeitplanungsregel ist daher eine konkrete Bestimmung dessen, was ein Job oder Thread darf oder nicht darf. Ein Verstoß gegen diese Bestimmung führt zu einer Ausnahmebedingung. In der Literatur über gemeinsamen Zugriff ist dieses Verfahren als Zwei-Phasen-Sperre bekannt. In einem Zwei-Phasen-Sperrschema muss ein Prozess im Voraus alle Sperren angeben, die es für eine bestimmte Task benötigt. Während der Ausführung der Verarbeitungsschritte ist es dann nicht mehr möglich, weitere Sperren zu setzen. Zwei-Phasen-Sperren eliminieren die Bedingung 'halten und warten', und somit die Voraussetzung für eine rückwirkende gegenseitige Sperre. Ein System, das nur Zeitplanungsregeln aus Basissperrelement einsetzt, kann sich also unmöglich selbst blockieren.