Инфраструктура параллельных задач

Одна из главных проблем при проектировании сложной системы - обеспечение отклика во время выполнения задач. В расширяемой системе, где компоненты, не предназначенные для совместной работы, совместно используют одни и те же ресурсы, эта сложность даже увеличивается. Эту проблему решает пакет org.eclipse.core.runtime.jobs, предусматривая инфраструктуру для планирования, выполнения и обслуживания параллельных задач. Эта инфраструктура основывается на использовании заданий, представляющих собой единицу работы, которая может выполняться асинхронно.

Задания

Задание - это основная асинхронная операция. Задание выполняется параллельно с другими заданиями. Для выполнения задачи модуль создает задание, а затем планирует его. Запланированное задание добавляется в очередь заданий, управляемую платформой. Для управления всеми ожидающими заданиями платформа использует фоновую нить планирования. Когда запущенные задания завершаются, нить удаляется из очереди, и платформа решает, какие задания будут выполняться дальше. При активизации задания платформа вызывает метод run(). Лучше всего это видно на простом примере:
   class TrivialJob extends Job {
      public TrivialJob() {
         super("Trivial Job");
      }
      public IStatus run(IProgressMonitor monitor) {
         System.out.println("Это задание");
         return Status.OK_STATUS;
      }
   }
Следующий фрагмент кода показывает создание и планирование задания:
   TrivialJob job = new TrivialJob();
   System.out.println("Планируется задание");
   job.schedule();
   System.out.println("Планирование завершено");
Вывод этой программы изменяется во времени. То есть невозможно гарантировать, что метод run задания будет выполняться для нити, создавшей и запланировавшей задание. Вывод будет либо таким:
   Планируется задание
   Это задание
   Планирование завершено
либо таким:
   Планируется задание
   Планирование завершено
   Это задание

Если требуется обеспечить, чтобы задание выполнилось перед продолжением работы программы, то можно воспользоваться методом join(). Он блокирует вызывающую программу до тех пор, пока выполнение задания не завершится или пока не будет прервана вызывающая нить. Перепишем фрагмент следующим образом:

   TrivialJob job = new TrivialJob();
   System.out.println("Планируется задание");
   job.schedule();
      job.join();
   if (job.getResult().isOk())
      System.out.println("Задание выполнено успешно");
   else
      System.out.println("Задание не выполнено");
Если вызов join() не прерывался, то этот метод гарантированно вернет следующий результат:
   Планируется задание
   Это задание
   Задание выполнено успешно

Конечно, если подключать задание сразу после его планирования, никакого параллелизма не будет. В этом случае можно точно так же работать с методом run прямо в вызывающей нити. Чуть позже мы рассмотрим несколько примеров ситуаций, в которых использование join оправдано.

В последнем фрагменте также используется результат задания. Результат - это объект IStatus, возвращаемый методом run(). Его можно применять для передачи из метода run любых необходимых объектов. Кроме этого, результат может служить для указания сбоя (IStatus возвращается с несколькими IStatus.ERROR) или отмены (IStatus.CANCEL).

Общие операции с заданиями

Мы знаем, как планировать задание и ожидать его завершения, но кроме этого, с заданиями можно сделать еще много интересного. Если вы запланировали задание, а потом решили, что оно больше не нужно, то задание можно остановить с помощью метода cancel(). Если отменяемое задание еще не успело запуститься, то оно сразу же сбрасывается и уже не запустится. Если же задание уже начало выполняться, то оно переходит в состояние ожидания отмены. При попытке отменить задание, ожидающее отмены, пригодится метод join(). Ниже приведен общий принцип для отмены задания и ожидания завершения его работы перед продолжением работы программы:

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

Если немедленная отмена невозможна, то cancel() вернет false, и вызывающая программа с помощью join() подождет успешной отмены задания.

Чуть менее агрессивен, чем отмена, метод sleep(). Точно так же, если задание еще не успело запуститься, то этот метод сразу же задержит его вызов. Задание по-прежнему будет в памяти платформы, и с помощью wakeUp() его можно добавить в очередь на выполнение.

Состояние задания

В течение своей жизни задание проходит через несколько состояний. Состоянием задания можно не только управлять через API с помощью методов cancel() и sleep(), оно также меняется, когда платформа запускает и завершает задание. Состояния задания могут быть следующими:

Перевести в спящее состояние можно только задание в состоянии WAITING. Если разбудить спящее задание, оно также перейдет в состояние WAITING. Отмена задания вернет его в состояние NONE.

Если необходимо знать состояние конкретного задания, то следует зарегистрировать получатель запросов изменения состояния, который будет оповещать об изменениях состояния задания. Это может пригодиться для отображения выполнения или, наоборот, отчета о задании.

Получатели запросов изменения задания

Для регистрации получателя запросов событий в конкретном задании можно воспользоваться методом Job addJobChangeListener. Получатель запросов IJobChangeListener определяет протокол для ответа на изменение состояния задания:

Во всех этих случаях получатель запросов предусматривает событие IJobChangeEvent, в котором указывается задание, состояние которого изменилось, и состояние его выполнения (если оно выполняется).

Примечание: Для получения текущего (относительного) состояния задания можно воспользоваться методом getState(). Однако этому результату не всегда можно доверять, поскольку задание запускает различные нити, и его состояние после получения ответа от метода может измениться. Для определения состояния задания рекомендуется все же пользоваться получателями запросов.

Диспетчер заданий

Протокол работы со всеми заданиями системы определяется интерфейсом IJobManager. В модулях, показывающих выполнение или каким-либо другим образом работающих с заданиями, можно использовать IJobManager. Этот интерфейс служит для таких задач, как приостановка всех заданий в системе, нахождение выполняющихся заданий, получение состояния выполнения конкретного задания. Диспетчер заданий платформы можно получить через API платформы:

   IJobManager jobMan = Platform.getJobManager();

В модулях, для которых важно знать состояние всех заданий в системе, можно не регистрировать отдельного получателя запросов изменения состояния для каждого задания, а зарегистрировать в диспетчере заданий один получатель запросов для всех.

Семейства заданий

Иногда удобнее, чтобы модуль работал с группой связанных заданий как с единым блоком. Это возможно с помощью семейств заданий. Для того, чтобы объявить задание членом какого-либо семейства, переопределяется метод belongsTo:

   public static final String MY_FAMILY = "myJobFamily";
   ...
   class FamilyJob extends Job {
      ...
      public boolean belongsTo(Object family) {
         return family == MY_FAMILY;
      }
   }
Для отмены, приоритизации, перевода в спящий режим и нахождения всех заданий семейства служит протокол IJobManager:
   IJobManager jobMan = Platform.getJobManager();
   jobMan.cancel(MY_FAMILY);
   jobMan.join(MY_FAMILY, null);

Так как семейства заданий представляются произвольными объектами, нужное состояние задания можно сохранять прямо в семействе, а объекты семейства создавать динамически по требованию. Во избежание случайных взаимодействий семейств, созданных разными модулями, очень важно, чтобы объекты семейств были уникальными.

С помощью семейств очень удобно искать группы заданий. Метод IJobManager.find(Object family) позволяет найти, например, все выполняющиеся, ожидающие или спящие задания на данный момент времени.