As regras de planejamento de tarefas podem ser utilizadas para controle quando suas tarefas são executadas em relação a outras tarefas. Em particular, as regras de planejamento permitem que você evite que várias tarefas sejam executadas ao mesmo tempo em situações em que a coincidência pode levar a resultados inconsistentes. Elas também permitem que você garanta o pedido de execução de uma série de tarefas. O poder de planejar regras é melhor ilustrado por um exemplo. Vamos começar com a definição de duas tarefas que são utilizadas para ligar e desligar um interruptor ao mesmo tempo:
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; } } }
Agora, criamos um programa simples que cria um interruptor e o liga e desliga novamente:
LightSwitch light = new LightSwitch(); light.on(); light.off(); System.out.println("The light is on? " + switch.isOn());
Se executarmos este programinha um número suficiente de vezes, finalmente obteremos a seguinte saída:
Apagando a luz Acendendo a luz A luz está acesa? true
Como isso pode acontecer? Pedimos que a luz acendesse e apagasse, então o estado final deveria ser apagada! O problema é que não houve nada que evitasse que a tarefa LightOff fosse executada ao mesmo tempo que a tarefa LightOn. Então, embora a tarefa "ligar" tenha sido planejada primeiro, a execução ao mesmo tempo significa que não há como prever a ordem de execução exata das duas tarefas simultâneas. Se a tarefa LightOff encerrar a execução antes da tarefa LightOn, obteremos este resultado inválido. O que precisamos é uma maneira de evitar que as duas tarefas sejam executadas ao mesmo tempo e é aí que as regras de planejamento entram.
Podemos corrigir este exemplo, criando uma regra de planejamento simples que age como um mutex (também conhecido como semáforo binário):
class Mutex implements ISchedulingRule { public boolean isConflicting(ISchedulingRule rule) { return rule == this; } public boolean contains(ISchedulingRule rule) { return rule == this; } }
Esta regra é incluída nas duas tarefas de interruptor do exemplo anterior:
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); } ... } }
Agora, quando as duas tarefas de interruptor são planejadas, a infra-estrutura de tarefa chama o método isConflicting para comparar as regras de planejamento das duas tarefas. Ele observará que as duas tarefas têm regras de planejamento em conflito e se certificará de que elas são executadas na ordem correta. Ele também se certificará de que elas nunca são executadas ao mesmo tempo. Se o programa de exemplo for executado um milhão de vezes, você sempre obterá o mesmo resultado:
Acendendo a luz Apagando a luz A luz está acesa? false
As regras também podem ser utilizadas independentemente das tarefas como um mecanismo de travamento geral. O seguinte exemplo adquire uma regra em um bloqueio try/finally, evitando que outros encadeamentos e tarefas sejam executados com essa regra na duração entre as chamadas de beginRule e endRule.
IJobManager manager = Platform.getJobManager(); try { manager.beginRule(rule, monitor); ... do some work ... } finally { manager.endRule(rule); }
É necessário ter muito cuidado ao adquirir e liberar regras de planejamento utilizando um padrão de codificação. Se houver falha ao encerrar uma regra para a qual você chamou beginRule, essa regra terá sido travada para sempre.
Embora a API de tarefa defina o contrato das regras de planejamento, ela não fornece implementações de regra de planejamento. Essencialmente, a infra-estrutura genérica não tem como saber quais conjuntos de tarefas estão ok para serem executadas ao mesmo tempo. Por padrão, as tarefas não têm regras de planejamento e uma tarefa planejada é executada tão rapidamente quando um encadeamento pode ser criado para executá-la.
Quando uma tarefa tem uma regra de planejamento, o método isConflicting é utilizado para determinar se a regra está em conflito com as regras que estão em execução atualmente. Assim, sua implementação de isConflicting pode definir exatamente quando é seguro executar sua tarefa. No exemplo do interruptor, a implementação de isConflicting simplesmente utiliza uma comparação de identidade com a regra fornecida. Se outra tarefa tem a regra idêntica, elas não serão executadas ao mesmo tempo. Ao gravar suas regras de planejamento, certifique-se de ler e seguir o contrato de API para isConflicting cuidadosamente.
Se a sua tarefa tem várias restrições não relacionadas, você pode compor várias regras de planejamento juntas utilizando uma MultiRegra. Por exemplo, se a sua tarefa precisa ligar um interruptor e além disso gravar informações em um soquete de rede, ela pode ter uma regra para o interruptor e uma regra para acesso de gravação ao soquete, combinada em uma única regra utilizando o método do depósito de informações do fornecedor MultiRule.combine.
Discutimos o método isConflicting em ISchedulingRule, mas até agora não mencionamos o método contains. Esse método é utilizado para um aplicativo especializado corretamente das regras de planejamento que muitos clientes não exigirão. As regras de planejamento podem ser compostas logicamente nas hierarquias para controlar o acesso aos recursos naturalmente hierárquicos. O exemplo mais simples para ilustrar esse conceito é um sistema de arquivos com base em árvore. Se um aplicativo deseja adquirir uma trava exclusiva em um diretório, normalmente significa que também deseja acesso exclusivo aos arquivos e aos subdiretórios neste diretório. O método contains é utilizado para especificar o relacionamento hierárquico entre as travas. Se você não precisar criar hierarquias de travas, você pode implementar o método contains para simplesmente chamar isConflicting.
Aqui está um exemplo de uma trava hierárquica para controlar o acesso de gravação aos controles de 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); } }
O método contains entra em reprodução se um encadeamento tentar adquirir uma segunda regra quando já tem uma. Para evitar a possibilidade de conflito, qualquer encadeamento fornecido pode ter somente uma regra de planejamento a qualquer momento. Se um encadeamento chama beginRule quando já possui uma regra, por uma chamada anterior de beginRule ou pela execução de uma tarefa com uma regra de planejamento, o método contains é consultado para verificar se as duas regras são equivalentes. Se o método contains para a regra já foi adquirido, ele retorna true, a chamada de beginRule será bem-sucedida. Se o método contains retorna false, ocorrerá um erro.
Para colocar isso em termos mais concretos, digamos que um encadeamento possui nossa regra FileLock de exemplo no diretório em "c:\temp". Enquanto possuir essa regra, só é permitido modificar arquivos dentro dessa subárvore de diretório. Se tentar modificar arquivos em outros diretórios que não estejam em "c:\temp", deve haver falha. Deste modo, uma regra de planejamento é uma especificação concreta para o que uma tarefa ou um encadeamento tem ou não permissão para fazer. Violar essa especificação resultará em uma exceção do tempo de execução. Na literatura de coincidência, essa técnica é conhecida como trava de duas fases. Em um esquema de trava de duas fases, um processo deve especificar, com antecedência, todas as travas necessárias para uma tarefa específica e não tem permissão para adquirir mais travas durante a operação. O travamento de duas fases elimina a condição de espera que é um pré-requisito para o conflito de espera circular. Portanto, é impossível para um sistema utilizando somente regras de planejamento como um travamento primitivo a digitação de um conflito.