As regras de agendamento de um trabalho podem ser usadas para controlar quando os trabalhos são executados relativamente a outros trabalhos. Em particular, as regras de agendamento permitem impedir que vários trabalhos sejam executados simultaneamente em situações em que a simultaneidade possa levar a resultados incoerentes. Também permitem garantir a ordem de execução de uma série de trabalhos. É melhor ilustrar o poder das regras de agendamento com um exemplo. Comecemos por definir dois trabalhos que são usados para acender e apagar a luz no interruptor simultaneamente:
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("Acender a luz"); } public IStatus run(IProgressMonitor monitor) { System.out.println("Acender a luz"); isOn = true; return Status.OK_STATUS; } } class LightOff extends Job { public LightOff() { super("Apagar a luz"); } public IStatus run(IProgressMonitor monitor) { System.out.println("Apagar a luz"); isOn = false; return Status.OK_STATUS; } } }
Agora criamos um programa simples que cria um interruptor e o acende e apaga outra vez:
LightSwitch light = new LightSwitch(); light.on(); light.off(); System.out.println("A luz está acesa? " + switch.isOn());
Se executarmos este pequeno programa vezes suficientes, acabaremos por ficar com a seguinte saída de dados:
Apagar a luz Acender a luz A luz está acesa? true
Como é que pode ser? Dissemos à luz para se acender e apagar, por isso o estado final devia ser apagado! O problema é que nada impedia que o trabalho LightOff fosse executado ao mesmo tempo que o trabalho LightOn. Por conseguinte, mesmo que o trabalho para "acender" (on) estivesse agendado primeiro, a execução simultânea dos trabalhos significa que não há maneira de prever a ordem de execução exacta dos dois trabalhos simultâneos. Se o trabalho LightOff terminar de executar antes do trabalho LightOn, teremos este resultado não válido. Precisamos de uma maneira de impedir que os dois trabalhos sejam executados simultaneamente, e é aqui que entram as regras de agendamento.
Podemos corrigir este exemplo criuando uma simples regra de agendamento que sirva de mutex (também denominadosemá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 em seguida é adicionada aos dois trabalhos de interruptor do exemplo anterior:
public class LightSwitch { final MutextRule rule = new MutexRule(); ... class LightOn extends Job { public LightOn() { super("Acender a luz"); setRule(rule); } ... } class LightOff extends Job { public LightOff() { super("Apagar a luz"); setRule(rule); } ... } }
Agora, quando os dois trabalhos de interruptor estiverem agendados, a infraestrutura de trabalhos chamará o método isConflicting para comparar as regras de agendamento dos dois trabalhos. Este irá perceber que os dois trabalhos têm regras de agendamento díspares e irá garantir que sejam executados pela ordem correcta. Também irá garantir que nunca sejam executados ao mesmo tempo. Agora se executar o programa exemplo um milhão de vezes, ficará sempre com o mesmo resultado:
Acender a luz Apagar a luz A luz está acesa? false
Também se pode utilizar regras independentemente de trabalhos como mecanismo de bloqueio geral. O exemplo seguinte adquire uma regra dentro de um bloco try/finally impedindo outros módulos e trabalhos de serem executados com essa regra durante as invocações entre beginRule e endRule.
IJobManager manager = Platform.getJobManager(); try { manager.beginRule(rule, monitor); ... realizar trabalho ... } finally { manager.endRule(rule); }
Deve ter-se o maior cuidado ao adquirir e libertar regras de agendamento com tal padrão de codificação. Se não conseguir terminar uma regra que tenha chamado com beginRule, terá bloqueado essa regra para sempre.
Embora a API de trabalhos defina o contrato das regras de agendamento, na realidade não proporciona nenhumas implementações de regras de agendamento. Essencialmente, a infra-estrutura genérica não tem maneira de saber quais os conjuntos de trabalhos que se podem executar simultaneamente. Por predefinição, os trabalhos não têm regras de agendamento, e um trabalho agendado é executado tão rápido quanto possa ser criado um módulo para o executar.
Quando um trabalho tem mesmo uma regra de agendamento, é utilizado o método isConflicting para determinar se a regra colide com as regras de outros trabalhos em execução. Deste modo, a implementação de isConflicting pode definir exactamente quando é que é seguro executar o trabalho. No nosso exemplo de interruptor, a implementação isConflicting utiliza simplesmente uma comparação de identidades com a regra facultada. Se houver outro trabalho com a regra idêntica, não serão executados em simultâneo. Ao escrever as suas próprias regras de agendamento, não deixe de ler e respeitar cuidadosamente o contrato de API para isConflicting.
Se o trabalho tiver várias restrições não relacionadas, poderá compor várias regras de agendamento juntas com uma MultiRule. Por exemplo, se o trabalho precisar de ligar um interruptor, e também de gravar informações num socket de rede, poderá ter uma regra para o interruptor e outra para acesso de escrita/gravação no socket, combinadas numa única regra que utilize o método de fábrica MultiRule.combine.
Abordámos o método isConflicting em ISchedulingRule, mas ainda não mencionámos o método contains. Este método é usado para uma aplicação razoavelmente especializada de regras de agendamento de que muitos clientes não necessitam. As regras de agendamento podem ser compostas logicamente em hierarquias para controlar o acesso a recursos naturalmente hierárquicos. O exemplo mais simples para ilustrar este conceito é um sistema de ficheiros baseado em árvore. Se uma aplicação quiser adquirir um bloqueio exclusivo sobre um directório, isto implica normalmente que também pretende acesso exclusivo aos ficheiros e subdirectórios nesse directório. O método contains é utilizado para especificar a relação hierárquica entre bloqueios. Se não for preciso criar hierarquias de bloqueios, poderá implementar o método contains para chamar simplesmente isConflicting.
De seguida é apresentado um exemplo de um bloqueio hierárquico para controlar acesso de escrita a pegas 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 acção se um módulo tentar adquirir segunda regra quando já tiver uma. Para evitar a possibilidade de impasse, um módulo só pode ter uma regra de agendamento em qualquer altura. Se um módulo chamar beginRule quando já tiver uma regra, quer através de chamada anterior de beginRule quer executando um trabalho com uma regra de agendamento, o método contains é consultado para ver se as duas regras são equivalentes. Se o método contains para a regra que já se tem devolver true, a invocação de beginRule será satisfatória. Se o método contains devolver false, ocorrerá um erro.
Em termos mais concretos, digamos que um módulo tem a nossa regra FileLock exemplo no directório em "c:\temp". Enquanto tiver esta regra, só poderá modificar ficheiros nessa sub-árvore de directórios. Se tentar modificar ficheiros noutros directórios que não estejam em "c:\temp", ocorrerá uma falha. Por conseguinte, uma regra de agendamento é uma especificação concreta do que um trabalhou ou módulo pode ou não fazer. Se violar essa especificação terá uma excepção de tempo de execução. Na literatura sobre simultaneidade, esta técnica chama-se bloqueio bifásico. Num esquema de bloqueio bifásico, é necessário que um processo especifique antecipadamente todos os bloqueios de que precisa para determinada tarefa, e depois não poderá adquirir mais bloqueios durante a operação. O bloqueio bifásico elimina a condição reter e esperar que é pré-requisito de impasses de espera circulares. Por conseguinte, é impossível que um sistema que só utilize regras de agendamento como primitivo de bloqueio entre num impasse.