Il est possible d'utiliser des règles de planification des tâches pour contrôler l'exécution des tâches vis-à-vis d'autres tâches. En particulier, les règles de planification vous permettent d'empêcher l'exécution de plusieurs tâches au même moment dans les cas où un accès concurrent peut conduire à des résultats incohérents. Elles vous permettent également de garantir l'ordre d'exécution d'une série de tâches. Voici un exemple qui permettra de mieux illustrer la puissance des règles de planification. Commençons par définir deux tâches utilisées tour à tour pour allumer et éteindre une lumière :
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("Extinction de la lumière"); } public IStatus run(IProgressMonitor monitor) { System.out.println("Turning the light off"); isOn = false; return Status.OK_STATUS; } } }
A présent, créons un programme simple permettant de créer un interrupteur et de l'utiliser pour allumer ou éteindre la lumière :
LightSwitch light = new LightSwitch(); light.on(); light.off(); System.out.println("The light is on? " + switch.isOn());
Si vous exécutez ce petit programme suffisamment de fois, vous finirez par obtenir le résultat suivant :
Extinction de la lumière Mise en marche de la lumière La lumière est allumée ? true
Comment cela est-il possible ? Vous avez allumé puis éteint la lumière, donc elle devrait être éteinte ! Le problème est que rien n'empêche l'exécution de la tâche LightOff en même temps que la tâche LightOn. Ainsi, même si la tâche "on" a été planifiée en premier, leur exécution concurrente signifie qu'il n'y a aucun moyen de prévoir l'ordre d'exécution exact des deux tâches concurrentes. Si la tâche LightOff est exécutée avant la tâche LightOn, vous obtenez ce résultat incorrect. Vous devez disposer d'un moyen d'empêcher l'exécution concurrente des deux tâches et c'est là que les règles de planification trouvent toute leur importance.
Il est possible de corriger cet exemple en créant une règle de planification simple qui se comporte comme un processus mutex (également appelé sémaphore binaire) :
class Mutex implements ISchedulingRule { public boolean isConflicting(ISchedulingRule rule) { return rule == this; } public boolean contains(ISchedulingRule rule) { return rule == this; } }
Cette règle est alors ajoutée aux deux tâches d'interrupteur de l'exemple précédent :
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("Extinction de la lumière"); setRule(rule); } ... } }
A présent, lorsque les deux tâches d'interrupteur sont planifiées, l'infrastructure des tâches appelle la méthode isConflicting afin de comparer les règles de planifications des deux tâches. L'infrastructure détecte un conflit entre les règles de planification et s'assure que les tâches seront exécutées dans l'ordre adéquat. Elle s'assure également qu'elles ne sont jamais exécutées en même temps. Maintenant, si vous exécutez l'exemple de programme un million de fois, vous obtenez toujours le même résultat :
Mise en marche de la lumière Extinction de la lumière La lumière est allumée ? false
Les règles peuvent également être utilisées indépendamment des tâches comme mécanisme de verrouillage général. L'exemple ci-dessous permet d'acquérir une règle dans un bloc "try/finally block", ce qui empêche l'exécution d'autres tâches et d'unités d'exécution pendant la durée comprise entre les appels de beginRule et de endRule.
IJobManager manager = Platform.getJobManager(); try { manager.beginRule(rule, monitor); ... do some work ... } finally { manager.endRule(rule); }
Vous devez faire preuve d'une attention extrême pour acquérir et libérer des règles de planification à l'aide de ce type de structure de code. Si vous ne parvenez pas à mettre fin à une règle pour laquelle vous avez appelé beginRule, la règle en question est verrouillée pour toujours.
Même si l'API de tâche définit le contrat des règles de planification, elle ne fournit pas d'implémentations de règle de planification. En substance, l'infrastructure générique ne dispose d'aucun moyen pour savoir quels sont les ensembles de tâches qui sont prêts pour une exécution concurrente. Par défaut, les tâches ne possèdent pas de règles de planification et une tâche planifiée est exécutée aussi rapidement qu'une unité d'exécution peut être créée à cet effet.
Lorsqu'une tâche dispose d'une règle de planification, la méthode isConflicting est utilisée pour déterminer si la règle entre en conflit avec les règles d'autres tâches exécutées en même temps. Ainsi, l'implémentation de isConflicting peut définir exactement un moment sûr pour exécuter la tâche. Dans l'exemple de l'interrupteur, l'implémentation isConflicting utilise simplement une comparaison d'identités avec la règle fournie. Si une autre tâche possède la même règle, elles ne seront pas exécutées simultanément. Lorsque vous écrivez vos propres règles de planification, veillez à lire et à suivre soigneusement le contrat API pour isConflicting.
Si la tâche possède plusieurs contraintes non liées, vous pouvez composer plusieurs règles de planification à l'aide d'une règle multiple. Par exemple, si la tâche doit activer un interrupteur et écrire des informations sur un socket réseau, elle peut posséder une règle pour l'interrupteur et une règle pour l'accès au socket en écriture, qui sont associées pour former une seule règle utilisant la méthode de fabrique MultiRule.combine.
Nous avons abordé la méthode isConflicting lorsque nous avons traité de la méthode ISchedulingRule, mais pour le moment, il n'a pas été fait mention de la méthode contains. Cette méthode est utilisée pour une application relativement spécialisée des règles de planification qu'un grand nombre de clients ne nécessitent pas. Les règles de planification peuvent être composées logiquement en hiérarchies pour contrôler l'accès à des ressources hiérarchiques par nature. L'exemple le plus simple pour illustrer ce concept est un système de fichiers sous forme d'arborescence. Si une application souhaite acquérir un verrou exclusif sur un répertoire, cela implique généralement un accès exclusif aux fichiers et aux sous-répertoires de ce répertoire. La méthode contains permet de spécifier la relation hiérarchique entre les verrous. Si vous n'avez pas besoin de créer des hiérarchies de verrous, vous pouvez implémenter la méthode contains pour appeler simplement isConflicting.
Voici un exemple de verrou hiérarchique permettant de contrôler l'accès en écriture aux descripteurs 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); } }
La méthode contains entre en jeu si une unité d'exécution tente d'acquérir une seconde règle alors qu'elle en possède déjà une. Pour éviter le risque de blocage, toute unité d'exécution donnée ne peut posséder qu'une seule règle de planification à la fois. Si une unité d'exécution appelle beginRule alors qu'elle possède déjà une règle, du fait de d'un appel précédent de beginRule ou suite à l'exécution d'une tâche sans règle de planification, la méthode contains est examinée pour savoir si les deux règles sont équivalentes. Si la méthode contains de la règle déjà possédée renvoie la valeur true, l'appel de beginRule aboutit. Si la méthode contains renvoie la valeur false, une erreur est générée.
Plus concrètement, une unité d'exécution possède notre exemple de règle FileLock dans le répertoire sous "c:\temp". Alors qu'il possède cette règle, il n'a l'autorisation de modifier des fichiers que dans cette arborescence secondaire de répertoires. S'il tente de modifier des fichiers dans d'autres répertoires qui ne se trouvent pas sous "c:\temp", l'opération échoue. Ainsi, une règle de planification est une spécification concrète indiquant ce qu'une tâche ou une unité d'exécution a l'autorisation ou non de faire. Si vous violez cette spécification, une exception d'exécution est générée. Pour ce qui des références à l'accès concurrent, cette technique est appelée verrouillage en deux phases. Dans une structure de verrouillage deux deux phases, un processus doit spécifier à l'avance tous les verrous nécessaires à une tâche particulière. Ensuite, il n'a pas la possibilité d'acquérir d'autres verrous au cours de l'opération. Le verrouillage en deux phases permet de supprimer la condition hold-and-wait qui constitue un pré-requis pour un verrou d'attente circulaire. Par conséquent, il est impossible pour un système utilisant des règles de planification uniquement comme primitive de verrouillage pour entrer dans un verrou.