Infraestructura de concurrencia

Uno de los retos principales de un sistema complejo consiste en mantener su capacidad de respuesta durante la realización de las tareas. Este reto es aún mayor en un sistema ampliable, cuando componentes no diseñados para ejecutarse conjuntamente comparten los mismos recursos. El paquete org.eclipse.core.runtime.jobs ofrece la solución a este reto al suministrar una infraestructura para planificar, ejecutar y gestionar operaciones de ejecución concurrentes. Esta infraestructura se basa en la utilización de trabajos para representar una unidad de trabajo que puede ejecutarse de forma asíncrona.

Trabajos

La clase Job representa una unidad de trabajo asíncrono que se ejecuta simultáneamente con otros trabajos. Para realizar una tarea, un conector crea un trabajo y, a continuación, lo planifica. Una vez planificado el trabajo, se envía a una cola de trabajos gestionada por la plataforma. La plataforma utiliza una hebra de planificación en segundo plano para gestionar todos los trabajos pendientes. Cuando un trabajo en ejecución finaliza, se elimina de la cola y la plataforma decide qué trabajo se ejecuta a continuación. Cuando se activa un trabajo, la plataforma invoca su método run(). El funcionamiento de los trabajos se observa con un ejemplo sencillo:
   class TrivialJob extends Job {
      public TrivialJob() {
         super("Trivial Job");
      }
      public IStatus run(IProgressMonitor monitor) {
         System.out.println("Esto es un trabajo");
         return Status.OK_STATUS;
      }
   }
En el siguiente fragmento de código se crea y planifica el trabajo:
   TrivialJob job = new TrivialJob();
   System.out.println("A punto de planificar un trabajo");
   job.schedule();
   System.out.println("Planificación del trabajo terminada");
La salida de este programa depende de la temporización. Es decir, no existe forma alguna de saber exactamente cuándo se ejecutará el método run del trabajo en relación a la hebra que ha creado el trabajo y lo ha planificado. La salida será:
   A punto de planificar un trabajo
   Esto es un trabajo
   Planificación del trabajo terminada
o bien:
   A punto de planificar un trabajo
   Planificación del trabajo terminada
   Esto es un trabajo

Si desea saber con certeza que un trabajo ha finalizado antes de continuar, puede utilizar el método join(). Este método bloqueará el llamador hasta que el trabajo haya finalizado o hasta que la hebra llamante se interrumpa. Vamos a reescribir el fragmento de código anterior de forma más determinista:

      TrivialJob job = new TrivialJob();
   System.out.println("A punto de planificar un trabajo");
   job.schedule();
      job.join();
   if (job.getResult().isOk())
      System.out.println("Trabajo finalizado satisfactoriamente");
   else
      System.out.println("El trabajo no ha finalizado satisfactoriamente");
Suponiendo que la llamada a join() no se interrumpa, este método devolverá con seguridad el siguiente resultado:
   A punto de planificar un trabajo
   Esto es un trabajo
   Trabajo finalizado satisfactoriamente

Evidentemente, por lo general no resulta de utilidad emplear join en un trabajo inmediatamente después de planificarlo, ya que al hacerlo no se obtendrá simultaneidad. En ese caso, también puede realizar el trabajo desde el método run del trabajo directamente en la hebra llamante. Más adelante observaremos algunos ejemplos en los que la utilización de join tiene más sentido.

El último fragmento también utiliza el resultado del trabajo. El resultado es el objeto IStatus devuelto desde el método run() del trabajo. Puede utilizar este resultado para pasar de nuevo los objetos necesarios desde el método run del trabajo. El resultado también puede utilizarse para indicar anomalía (devolviendo un IStatus con la gravedad IStatus.ERROR) o cancelación (IStatus.CANCEL).

Operaciones comunes de trabajos

hemos visto cómo planificar un trabajo y esperar a que finalice, pero con los trabajos pueden realizarse muchas otras operaciones interesantes. Si planifica un trabajo pero decide que ya no es necesario, puede detenerlo mediante el método cancel(). Si el trabajo aún no se ha iniciado cuando se cancela, se eliminará inmediatamente y no se ejecutará. Por otra parte, si el trabajo ya se ha iniciado, queda a elección de éste el hecho de responder a la cancelación. Al intentar cancelar un trabajo, resulta más cómodo utilizar el método join(). A continuación figura un código habitual para cancelar un trabajo y esperar a que finalice antes de continuar:

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

Si la cancelación no se realiza inmediatamente, cancel() devolverá false y el llamador utilizará join() para esperar a que el trabajo se cancele satisfactoriamente.

El método sleep() es ligeramente menos drástico que la cancelación. De nuevo, si el trabajo aún no se ha iniciado, este método lo colocará en estado de retención indefinida. La plataforma seguirá recordando el trabajo, y una llamada a wakeUp() provocará la adición del trabajo a la cola de espera, en la que finalmente se ejecutará.

Estados de trabajo

Un trabajo pasa por varios estados durante su ciclo de vida. No sólo puede manipularse mediante API como cancel() y sleep(), sino que su estado también cambia a medida que la plataforma lo ejecuta y finaliza. Los trabajos pueden pasar por los siguientes estados:

Un trabajo sólo puede colocarse en estado de latencia si su estado actual es WAITING. El hecho de despertar un trabajo latente lo colocará de nuevo en estado WAITING. La cancelación de un trabajo lo devolverá al estado NONE.

Si el conector necesita saber el estado de un trabajo determinado, puede registrar un escuchador de cambio de trabajo que recibirá información de los cambios de estado del trabajo a lo largo de su ciclo de vida. Esto resulta de utilidad para mostrar el progreso o informar de un trabajo.

Escuchas de cambios de trabajo

Puede utilizarse el método addJobChangeListener de Job para registrar un escuchador de un trabajo determinado. IJobChangeListener define el protocolo para responder a los cambios de estado de un trabajo:

En todos estos casos, se suministra al escuchador un evento de tipo IJobChangeEvent que especifica el trabajo que sufre el cambio de estado y el estado en que finaliza (si es el caso).

Nota: los trabajos también definen el método getState() para obtener el estado (relativamente) actual de un trabajo. Sin embargo, este resultado no siempre es fiable, ya que los trabajos se ejecutan en una hebra diferente y pueden haber cambiado de nuevo de estado en el momento del retorno de la llamada. Los escuchas de cambios de trabajo son el mecanismo recomendado para descubrir los cambios de estado de un trabajo.

El gestor de trabajos

IJobManager define un protocolo para trabajar con todos los trabajos del sistema. Los conectores que muestran el progreso o trabajan con la infraestructura de trabajos pueden utilizar IJobManager para realizar tareas tales como suspender todos los trabajos del sistema, buscar el trabajo que se está ejecutando o recibir información de progreso relativa a un trabajo determinado. El gestor de trabajos de la plataforma puede obtenerse mediante la API Platform:

      IJobManager jobMan = Platform.getJobManager();

Los conectores interesados en el estado de todos los trabajos del sistema pueden registrar un escuchador de cambios de trabajo en el gestor de trabajos en lugar de registrar escuchas de muchos trabajos individuales.

Familias de trabajos

A veces resulta más fácil para un conector trabajar con un grupo de trabajos relacionados como una sola unidad. Esto puede realizarse mediante familias de trabajos. Un trabajo declara que pertenece a una determinada familia alterando temporalmente el método belongsTo:

   public static final String MY_FAMILY = "myJobFamily";
   ...
   class FamilyJob extends Job {
      ...
      public boolean belongsTo(Object family) {
         return family == MY_FAMILY;
      }
   }
El protocolo IJobManager puede utilizarse para cancelar, unir, situar en estado de latencia o buscar todos los trabajos de una familia:
   IJobManager jobMan = Platform.getJobManager();
   jobMan.cancel(MY_FAMILY);
   jobMan.join(MY_FAMILY, null);

Dado que las familias de trabajos se representan mediante objetos arbitrarios, puede almacenar información de estado interesante en la propia familia de trabajos, y los trabajos pueden construir dinámicamente objetos de familia cuando es necesario. Es importante utilizar objetos de familia que sean prácticamente exclusivos para evitar interacciones accidentales con las familias creadas por otros conectores.

Las familias también son adecuadas para localizar grupos de trabajos. Puede utilizarse el método IJobManager.find(Object family) para localizar instancias de todos los trabajos en ejecución, espera y latencia en cualquier momento.