С помощью правил планирования задания можно управлять временем запуска задания по отношению к другим заданиям. В частности, правила планирования позволяют предотвратить одновременный запуск нескольких заданий в ситуациях, когда это нежелательно. Также с помощью правил планирования можно обеспечить порядок выполнения серии заданий. Лучше всего возможности правил планирования иллюстрируются на примере. Сначала определяем два задания, которые одновременно включают и выключают свет:
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("Включение света"); 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("Выключение света"); isOn = false; return Status.OK_STATUS; } } }
Теперь напишем простую программу, которая создает выключатель света, включает его и выключает снова:
LightSwitch light = new LightSwitch(); light.on(); light.off(); System.out.println("Свет горит? " + switch.isOn());
Если запустить эту программу достаточное количество раз, то мы получим следующее:
Выключение света Включение света Свет горит? true
Как такое может быть? Мы же включили свет, а потом выключили, значит, последним его состоянием должно быть выключенное! Дело в том, что задания LightOff и LightOn сработали одновременно. Поэтому, даже если задание "on" запланировано первым, все равно параллельное выполнение означает, что порядок срабатывания двух параллельных заданий предсказать невозможно. Если задание LightOff завершит выполнение раньше, чем LightOn, то мы получим ошибочный результат. Поэтому в такой ситуации необходимо предотвратить одновременное выполнение двух заданий, и здесь приходят на помощь правила планирования.
Этот пример можно исправить, создав простое правило планирования, которое будет работать как mutex, или взаимная блокировка (иногда еще называется двоичный семафор):
class Mutex implements ISchedulingRule { public boolean isConflicting(ISchedulingRule rule) { return rule == this; } public boolean contains(ISchedulingRule rule) { return rule == this; } }
Добавляем это правило к обоим заданиям предыдущего примера:
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); } ... } }
Теперь, если запланированы два "световых" задания, инфраструктура задания будет вызывать метод isConflicting, который сравнивает правила планирования этих заданий. Он оповестит, что правила для этих заданий взаимно конфликтуют, и обеспечит их выполнение в надлежащем порядке. Этот метод позволяет предотвратить одновременное выполнение этих заданий. Итак, если теперь запустить эту программу-пример хоть миллион раз, результат будет всегда один и тот же:
Включение света Выключение света Свет горит? false
Правила можно использовать и независимо от задания, в качестве общего механизма блокировки. В следующем примере рассматривается правило в блоке try/finally, предотвращающее выполнение других нитей и заданий с этим же правилом в период между вызовами beginRule и endRule.
IJobManager manager = Platform.getJobManager(); try { manager.beginRule(rule, monitor); ... do some work ... } finally { manager.endRule(rule); }
Будьте чрезвычайно осторожны, применяя для запрашивания и освобождения правил планирования шаблон кода. Если вы ошибетесь с концом правила, для которого был вызван метод beginRule, вы заблокируете правило навсегда.
Несмотря на то, что в API задания определено соглашение по правилам планирования, все же реализация этих правил в нем сделана не полностью. Например, в общей инфраструктуре нет возможности узнать, какие группы заданий могут выполняться параллельно. По умолчанию у задания нет правил планирования и оно выполняется тогда, когда будет создана нить для его запуска.
Если у задания есть правило планирования, то для определения конфликта этого правила с правилами других выполняющихся заданий можно воспользоваться методом isConflicting. То есть метод isConflicting помогает точно определить, безопасно ли сейчас выполнять какое-либо задание. В "световом" примере метод isConflicting просто сравнивает субъект с предоставленным правилом. Если есть другое задание с таким же правилом, то делается вывод, что параллельно их выполнять нельзя. При написании собственных правил внимательно прочтите соглашение API для isConflicting и следуйте ему.
Если у вашего задания есть несколько не связанных между собой ограничений, то можно объединить несколько правил планирования вместе с помощью метода MultiRule. Например, если вашему заданию нужно включить свет и одновременно записать информацию в сетевой сокет, то для него можно создать правило для света и правило получения права на запись в сокет, объединенные в одно правило с помощью метода фабрики MultiRule.combine.
При обсуждении метода isConflicting класса ISchedulingRule ничего не было сказано о методе contains. Этот метод служит для направленного применения правил планирования, не требующих многих клиентов. Правила планирования можно логически упорядочить в иерархии. Это позволит управлять доступом к ресурсам, действительно подчиняющимся определенной иерархии. Самый простой пример, иллюстрирующий эту концепцию - это древовидная файловая система. Если приложению требуется захватить (заблокировать) каталог для исключительного пользования, это подразумевает и исключительный доступ ко всем файлам и подкаталогам этого каталога. С помощью метода contains можно указать иерархические отношения между этими блокировками. Если иерархия блокировок не нужна, то метод contains может использоваться просто для вызова isConflicting.
Ниже приведен пример иерархической блокировки, позволяющей управлять доступом на запись в описатели 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); } }
Метод contains вступает в игру, если нить пытается захватить второе правило, уже владея первым. Во избежание возможного тупика нить может владеть только одним правилом планирования в один момент времени. Если нить вызывает beginRule, когда у нее уже есть правило (либо в результате предыдущего вызова beginRule, либо в результате выполнения задания с правилом планирования), метод contains будет смотреть, не эквивалентны ли два правила. Если метод contains для правила, уже находящегося в собственности, вернул true, то вызов beginRule будет успешным. Если же contains вернет false, то возникнет ошибка.
Конкретнее на примере: допустим, нить владеет правилом FileLock в каталоге "c:\temp". Пока она им владеет, ей позволено только изменять файлы в поддереве этого каталога. Если нить попытается изменить файлы в другом каталоге, не находящемся в "c:\temp", то это у нее не выйдет. Следовательно, правило планирования - это конкретная спецификация, указывающая, что можно делать нити или заданию, а что нельзя. Нарушение этих правил вызывает исключительную ситуацию времени выполнения. В литературе по параллельному выполнению такая технология называется двухстадийное блокирование. В схеме двухстадийного блокирования для процесса следует заблаговременно указать все блокировки, которые ему потребуются, поскольку других блокировок он во время выполнения захватить не сможет. Двухстадийное блокирование исключает ситуацию "захватил-и-жди", являющуюся непременным условием для взаимного блокирования. Следовательно, тупиковые ситуации для системы, использующей в качестве простых блокировок только правила планирования, невозможны.