Infra-estrutura de simultaneidade

Um dos maiores desafios de um sistema complexo consiste em permanecer contactável enquanto decorre a execução de tarefas. O desafio é ainda maior num sistema extensível, quando componentes que não foram concebidos para serem executados juntos partilham os mesmos recursos. O pacote org.eclipse.core.runtime.jobs destina-se a este desafio pois proporciona infra-estrutura para agendar, executar e gerir operações executadas em simultâneo. Esta infra-estrutura baseia-se na utilização de trabalhos para representar uma unidade de trabalho que pode ser executada de modo assíncrono.

Trabalhos

A classe Job representa uma unidade de trabalho assíncrono executada em simultâneo com outros trabalhos. Para executar uma tarefa, um plug-in cria um trabalho e depois agendada-o. Uma vez agendado, o trabalho é adicionado a uma fila de trabalhos gerida pela plataforma. A plataforma utiliza um módulo de agendamento em segundo plano para gerir todos os trabalhos pendentes. Quando um trabalho em execução se conclui, é removido da fila e a plataforma decide qual o próximo a executar. Quando um trabalho fica activo, a plataforma invoca o respectivo método run(). É melhor demonstrar os trabalhos com um exemplo simples:
class TrivialJob extends Job {
      public TrivialJob() {
         super("Trabalho Trivial");
      }
         public IStatus run(IProgressMonitor monitor) {
         System.out.println("É um trabalho");
            return Status.OK_STATUS;
      }
   }
O trabalho é criado e agendado na porção de código seguinte:
   TrabalhoTrivial job = new TrabalhoTrivial();
   System.out.println("Vamos agendadar um trabalho");
   job.schedule();
   System.out.println("Terminámos de agendadar um trabalho");
A saída de dados deste programa depende da oportunidade, ou seja, não há maneira de garantir que o método run do trabalho seja executado relativamente ao módulo que criou o trabalho e o agendadou. A saída de dados será:
   Vamos agendadar um trabalho
   É um trabalho
   Terminámos de agendadar um trabalho
or:
   Vamos agendadar um trabalho
   Terminámos de agendadar um trabalho
   É um trabalho

Se quiser ter a certeza de que um trabalho foi concluído antes de prosseguir, poderá utilizar o método join(). Este método irá bloquear o chamador até que o trabalho seja concluído ou até que o módulo de chamada seja interrompido. Vamos reescrever a porção de código supra de maneira mais determinista:

      TrabalhoTrivial job = new TrabalhoTrivial();
   System.out.println("Vamos agendadar um trabalho");
   job.schedule();
      job.join();
   if (job.getResult().isOk())
      System.out.println("Trabalho satisfatoriamente concluído");
    else
      System.out.println("Trabalho não concluído satisfatoriamente");
Partindo do princípio de que a chamada join() não é interrompida, este método garante a devolução do seguinte resultado:
   Vamos agendadar um trabalho
   É um trabalho
   Trabalho satisfatoriamente concluído

Naturalmente que, regra geral, não é útil juntar um trabalho imediatamente depois de o agendadar, dado que não se chega a simultaneidade nenhuma com isto. Neste caso, bem podemos realizar o trabalho a partir do método run directamente no módulo de chamada. Veremos alguns exemplos mais tarde em que o uso da junção (join) faz mais sentido.

A última porção de código também utiliza o resultado do trabalho. O resultado é o objecto IStatus que é devolvido pelo método run() do trabalho. Poderá utilizar este resultado para retransmitir objectos necessários a partir do método run do trabalho. O resultado também pode ser usado para indicar falha (devolvendo um IStatus com a gravidade IStatus.ERROR) ou o cancelamento (IStatus.CANCEL).

Operações comuns de trabalhos

Já vimos como agendadar um trabalho e esperar pela sua conclusão, mas há muito mais coisas interessantes a fazer com trabalhos. Se agendadar um trabalho mas depois decidir que já não precisa dele, poderá pará-lo com o método cancel(). Se o trabalho ainda não tiver começado a executar quando for cancelado, será imediatamente descartado e não executado. Se, por outro lado, o trabalho já tiver começado a executar, compete ao mesmo reagir ou não ao cancelamento. Quando tentamos cancelar um trabalho, o facto de podermos esperar por ele com o método join() revela-se muito útil. De seguida é apresentada uma maneira comum de cancelar um trabalho, e de esperar até o mesmo terminar antes de prosseguir:

   if (!job.cancel())
      job.join();

Se o cancelamento não entrar em vigor imediatamente, o método cancel() irá devolver false e o chamador utilizará join() para esperar que o trabalho seja satisfatoriamente cancelado.

Ligeiramente menos drástico do que o cancelamento, temos o método sleep(). Mais uma vez, se o trabalho ainda não tiver começado a executar, este método fará com que o mesmo seja retido indefinidamente. O trabalho ainda assim será recordado pela plataforma, e uma chamada wakeUp() fará com o mesmo seja adicionado à fila de espera, onde acabará por ser executado.

Estados de trabalhos

Um trabalho passar por vários estados durante o seu tempo de vida. Pode não só ser manipulado mediante APIs como, por exemplo, cancel() e sleep(), como também se lhe muda o estado consoante a plataforma o executa e conclui. Os trabalhos podem passar pelos estados seguintes:

Só se pode colocar um trabalho em estado dormente se estiver à espera (WAITING). Quando acordamos um trabalho dormente este também fica em estado WAITING. Cancelar um trabalho devolve-o ao estado NONE.

Se o plug-in precisar de saber o estado de determinado trabalho, poderá registar um ouvinte de alteração de trabalhos que é notificado à medida que o trabalho passa pelo seu ciclo de vida. Isto é útil para mostrar progresso ou reportar outras ocorrências do trabalho.

Ouvintes de alteração de trabalhos

O método JobaddJobChangeListener pode ser usado para registar um ouvinte em determinado trabalho. OIJobChangeListener define protocolo para reagir às alterações de estado num trabalho:

Em todos estes casos, o ouvinte dispõe de um IJobChangeEvent que especifica o trabalho a passar pela alteração de estado e o estado da sua conclusão (se tiver ocorrido).

Nota: Os trabalhos também definem o método getState() para obter o estado (relativamente) actual de um trabalho. Todavia, este resultado nem sempre é fiável, dado que os trabalhos são executados num módulo diferente e poderão mudar de estado novamente na altura do retorno da chamada. Os ouvintes de alterações de trabalhos são o mecanismo recomendado para descobrir alterações de estado num trabalho.

O gestor de trabalhos

O IJobManager define protocolo para trabalhar com todos os trabalhos do sistema. Os plug-ins que mostram progresso ou trabalham de outro modo com a infra-estutura do trabalho podem utilizar IJobManager para realizar tarefas como, por exemplo, suspender todos os trabalhos no sistema, saber qual o trabalho em execução ou receber comentários de progresso sobre determinado trabalho. O gestor de trabalhos da plataforma pode ser obtido com a API Platform:

      IJobManager jobMan = Platform.getJobManager();

Os plug-ins interessados no estado de todos os trabalhos no sistema podem registar um ouvinte de alteração de trabalhos junto do gestor de trabalhos, em vez de registarem ouvintes em muitos trabalhos individuais.

Famílias de trabalhos

Por vezes é mais fácil para um plug-in trabalhar com um grupo de trabalhos relacionados enquanto unidade. Tal pode realizar-se com famílias de trabalhos. Um trabalho declara que pertence a certa família sobrepondo o método belongsTo:

   public static final String A_MINHA_FAMÍLIA = "aMinhaFamíliaTrabalhos";
   ...
   class FamilyJob extends Job {
      ...
      public boolean belongsTo(Object family) {
         return family == A_MINHA_FAMÍLIA;
      }
   }
Pode ser utilizado o protocolo IJobManager para cancelar, juntar, colocar dormente ou localizar todos os trabalhos numa família:
   IJobManager jobMan = Platform.getJobManager();
   jobMan.cancel(A_MINHA_FAMÍLIA);
   jobMan.join(A_MINHA_FAMÍLIA, null);

Dado que as famílias de trabalhos são representadas mediante objectos arbitrários, poderá armazenar estados interessantes na própria família de trabalhos, e os trabalhos podem construir dinamicamente objectos de família conforme o necessário. É importante utilizar objectos de família que sejam razoavelmente únicos, de modo a evitar interacções acidentais com as famílias criadas por outros plug-ins.

As famílias também são uma maneira conveniente de localizar grupos de trabalhos. O método IJobManager.find(Object family) pode ser utilizado para localizar instâncias de todos os trabalhos em execução, em espera e dormentes em qualquer altura.