Las normas de planificación de un trabajo pueden utilizarse para controlar cuándo se ejecutan los trabajos en relación a otros. En particular, las normas de planificación permiten evitar que se realicen simultáneamente varios trabajos en situaciones en las que la simultaneidad puede conducir a resultados incoherentes. También permiten garantizar el orden de ejecución de una serie de trabajos. La capacidad de las normas de planificación se observará mejor mediante un ejemplo. Empecemos por definir dos trabajos utilizados para encender y apagar un interruptor simultáneamente:
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("Encender la luz"); } public IStatus run(IProgressMonitor monitor) { System.out.println("Encender la luz"); isOn = true; return Status.OK_STATUS; } } class LightOff extends Job { public LightOff() { super("Apagar la luz"); } public IStatus run(IProgressMonitor monitor) { System.out.println("Apagar la luz"); isOn = false; return Status.OK_STATUS; } } }
Ahora crearemos un programa sencillo que crea un interruptor y lo enciende y apaga de nuevo:
LightSwitch light = new LightSwitch(); light.on(); light.off(); System.out.println("¿Está la luz encendida? " + switch.isOn());
Si ejecutamos este pequeño programa varias veces, finalmente obtendremos la siguiente salida:
Apagando la luz Encendiendo la luz ¿Está la luz encendida? true
¿Cómo puede darse esta salida? Hemos indicado a la luz que se encendiera y luego se apagara, y por tanto su estado final debería ser apagada. El problema es que no había nada que impidiera que el trabajo LightOff se ejecutará al mismo tiempo que el trabajo LightOn. Así, aunque el trabajo "on" estaba planificado en primer lugar, su ejecución simultánea significa que no hay forma de predecir el orden de ejecución exacto de los dos trabajos simultáneos. Si el trabajo LightOff finaliza la ejecución antes del trabajo LightOn, obtendremos este resultado no válido. Lo que necesitamos es una forma de evitar que dos trabajos se ejecuten simultáneamente, y aquí es donde intervienen las normas de planificación.
Podemos arreglar este ejemplo creando una norma de planificación simple que actúe como mutex (también conocido como semáforo binario):
class Mutex implements ISchedulingRule { public boolean isConflicting(ISchedulingRule rule) { return rule == this; } public boolean contains(ISchedulingRule rule) { return rule == this; } }
A continuación, esta norma se añade a los dos trabajos de conmutación de luz del ejemplo anterior:
public class LightSwitch { final MutextRule rule = new MutexRule(); ... class LightOn extends Job { public LightOn() { super("Encender la luz"); setRule(rule); } ... } class LightOff extends Job { public LightOff() { super("Apagar la luz"); setRule(rule); } ... } }
Ahora, cuando se planifiquen los dos trabajos de conmutación de luz, la infraestructura de trabajos llamará al método isConflicting para comparar las normas de planificación de los dos trabajos. Observará que los dos trabajos tienen normas de planificación en conflicto y se asegurará de que se ejecuten en el orden correcto. También se asegurará de que nunca se ejecuten simultáneamente. Ahora, si ejecuta el ejemplo un millón de veces, obtendrá siempre el mismo resultado:
Encendiendo la luz Apagando la luz ¿Está la luz encendida? false
Las normas también pueden utilizarse independientemente de los trabajos, como mecanismo de bloqueo general. En el ejemplo siguiente se adquiere una norma dentro de un bloque try/finally, que impide que otras hebras y trabajos se ejecuten con esa norma durante el período entre invocaciones de beginRule y endRule.
IJobManager manager = Platform.getJobManager(); try { manager.beginRule(rule, monitor); ... realizar algún trabajo ... } finally { manager.endRule(rule); }
Debe tener extremo cuidado al adquirir y liberar normas de planificación mediante un patrón de planificación de este tipo. Si no consigue finalizar una norma para la que ha llamado a beginRule, habrá bloqueado esa norma para siempre.
Aunque la API de trabajos define el contrato de normas de planificación, en realidad no suministra ninguna implementación de normas de planificación. Esencialmente, la infraestructura genérica no tiene forma alguna de saber qué conjuntos de trabajos son adecuados para ejecutarse simultáneamente. Por omisión, los trabajos no tienen normas de planificación, y un trabajo planificado se ejecuta en cuanto puede crearse una hebra para ejecutarlo.
Si un trabajo tiene una norma de planificación, se utiliza el método isConflicting para determinar si la norma está en conflicto con las normas de algún trabajo que se esté ejecutando actualmente. Por tanto, la implementación de isConflicting puede definir exactamente cuándo es seguro ejecutar un trabajo. En el ejemplo del interruptor, la implementación de isConflicting utiliza simplemente una comparación de identidades con la norma suministrada. Si otro trabajo tiene una norma idéntica, los trabajos no se ejecutarán simultáneamente. Al escribir normas de planificación propias, asegúrese de leer y seguir cuidadosamente el contrato de API correspondiente a isConflicting.
Si el trabajo tiene varias restricciones no relacionadas, puede componer varias normas de planificación conjuntamente mediante MultiRule. Por ejemplo, si el trabajo debe encender un interruptor y también escribir información en un socket de red, puede tener una norma para el interruptor y una norma para el acceso de escritura al socket, combinadas en una única norma mediante el método de fábrica MultiRule.combine.
Hemos descrito el método isConflicting de ISchedulingRule, pero hasta ahora no hemos mencionado el método contains. Este método se utiliza para una aplicación bastante especializada de normas de planificación que no requerirán muchos clientes. Las normas de planificación pueden componerse lógicamente en jerarquías para controlar el acceso a recursos que son jerárquicos por naturaleza. El ejemplo más simple que ilustra este concepto es un sistema de archivos basado en árbol. Si una aplicación desea adquirir un bloqueo exclusivo sobre un directorio, esto implica generalmente que también desea acceso exclusivo a los archivos y subdirectorios de ese directorio. El método contains se utiliza para especificar la relación jerárquica entre los bloqueos. Si no necesita crear jerarquías de bloqueos, puede implementar el método contains para que llame simplemente a isConflicting.
A continuación figura un ejemplo de un bloqueo jerárquico que controla el acceso de escritura a handles 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); } }
El método contains interviene si una hebra intenta adquirir una segunda norma cuando ya es propietaria de una. Para evitar la posibilidad de que se produzca un punto muerto, una hebra determinada sólo puede ser propietaria de una norma de planificación en un momento dado. Si una hebra llama a beginRule cuando ya es propietaria de una norma, ya sea mediante una llamada anterior a beginRule o ejecutando un trabajo con una norma de planificación, se consulta al método contains para averiguar si las dos normas son equivalentes. Si el método contains de la norma que ya es propiedad de la hebra devuelve true, la invocación a beginRule será satisfactoria. Si el método contains devuelve false, se producirá un error.
Para situar esta descripción sobre el papel, supongamos que una hebra es propietaria de la norma de ejemplo FileLock del directorio que se encuentra en "c:\temp". Mientras sea propietaria de esta norma, sólo podrá modificar archivos situados dentro de ese subárbol de directorios. Si intenta modificar archivos de otros directorios que no estén bajo "c:\temp", fallará. Por tanto, una norma de planificación es una especificación concreta de lo que un trabajo o hebra puede o no puede hacer. La violación de dicha especificación provocará una excepción de tiempo de ejecución. En la terminología de la simultaneidad, esta técnica se conoce como bloqueo de dos fases. En un esquema de bloqueo de dos fases, un proceso especifica por anticipado todos los bloqueos que necesitará para una tarea determinada, y no podrá adquirir más bloqueos durante la operación. El bloqueo de dos fases elimina la condición de retención y espera que es un prerrequisito de los puntos muertos de espera circulares. Por tanto, es imposible que un sistema que sólo utilice normas de planificación como primitivo de bloqueo entre en un punto muerto.