При участии Тима Перлса, Джошуа Блоха, Джозева Боубира, Дэвида Холмса и Дага Ли Параллельное программирование в java на
Скачать 4.97 Mb.
|
Глава 7 Отмена и завершение Запускать задачи и потоки довольно легко. Большую часть времени мы позволяем им самим принимать решение о том, когда остановиться, позволяя работать до завершения. Однако, иногда мы хотим остановить выполнение задач или потоков раньше, чем они остановились бы сами по себе, возможно, в связи с тем, что пользователь отменил операцию или приложение должно быстро завершиться. Безопасно, быстро и надежно остановить задачу или поток не всегда легко. Язык Java не предоставляет никакого механизма для безопасной принудительной остановки потока, вместо этого Java предоставляет прерывание (interruption) - механизм взаимодействия, позволяющий одному потоку попросить другой поток прекратить выполнять то, что он делает 81 Кооперативный подход необходим, потому что достаточно редко возникает необходимость в том, чтобы задача, поток или служба немедленно останавливались, так как это может привести к тому, что совместно используемые структуры данных могут остаться в несогласованном состоянии. Вместо этого задачи и службы можно закодировать так, чтобы по запросу они “очищали” все выполняемые в данный момент работы, а затем завершали их. Такой подход обеспечивает большую гибкость, поскольку сам код задачи, как правило, лучше способен оценить требуемые действия по очистке используемых ресурсов, чем код, запрашивающий отмену. Проблемы, связанные с завершением жизненного цикла, могут усложнить проектирование и реализацию задач, служб и приложений, и этот важный элемент разработки программ слишком часто игнорируется. Работа со сбоями, завершением работы и отменой - это одна из характеристик, отличающих корректное приложение от просто работающего приложения. В этой главе рассматриваются механизмы отмены и прерывания, а также кодирование задач и служб для перехвата и обработки запросов на отмену. 7.1 Отмена задачи Выполнение активности может быть отменено, если внешний код может подвести её к завершению до нормального завершения. Существует ряд причин, по которым может потребоваться отменить выполнение активности: Отмена, инициированная пользователем. Пользователь нажал на кнопку "отмена" в приложении с GUI или запросил отмену через интерфейс управления, такой как JMX (Java Management Extensions). Активности, ограниченные по времени. Приложение занимается поиском решения в пространстве проблем на протяжении конечного промежутка времени и выбирает лучшее решение, найденное за это время. По истечении таймера все задачи поиска отменяются. События приложения. Приложение ищет решение в пространстве проблем путем разложения его на отдельные задачи так, что различные задачи выполняют 81 Устаревшие методы Thread stop и suspend являлись попыткой реализовать такой механизм, но довольно скоро были осознаны серьезные недостатки, связанные с ними, поэтому их использования следует избегать. За разъяснениями обратитесь к статье http://java.sun.com/j2se/1.5.0/docs/guide/misc/threadPrimitiveDeprecation.html , в которой рассматриваются проблемы связанные с использованием этих методов. поиск в различных регионах пространства проблем. Когда одна задача находит решение, все остальные задачи, которые еще находятся в состоянии поиска, отменяются. Ошибки. Веб-сканер выполняет поиск соответствующих страниц, сохраняя страницы или сводные данные на диске. Когда задача сканера сталкивается с ошибкой (например, диск переполнен), другие задачи обхода отменяются, возможно, с записью своего текущего состояния, чтобы их можно было перезапустить позже. Завершение работы. Когда приложение или служба завершает свою работу, что- то должно быть сделано с работой, выполняющейся в текущий момент или ожидающей в очереди на выполнение. При плавном завершении работы, задачам, выполняющимся в текущий момент, может быть разрешено завершить свою работу; при немедленном завершении работы, текущие задачи могут быть отменены. В Java нет безопасного способа превентивной остановки потока, и поэтому нет безопасного способа превентивной остановки задачи. Существуют лишь механизмы кооперации, в рамках которых задача и код запрашивающий отмену, следуют согласованному протоколу. Одним из таких механизмов кооперации является установка флага "запрос отмены", периодически проверяемого задачей; если задача обнаруживает, что флаг установлен, задача завершается раньше. Этот подход иллюстрируется классом PrimeGenerator из листинга 7.1, который перечисляет простые числа до тех пор, пока его работа не будет отменена. Метод cancel устанавливает флаг cancelled , и главный цикл опрашивает состояние этого флага перед поиском следующего простого числа. (Для надежной работы переменная cancelled должна быть объявлена с ключевым словом volatile .) @ThreadSafe public class PrimeGenerator implements Runnable { @GuardedBy("this") private final List = new ArrayList BigInteger p = BigInteger.ONE; while (!cancelled) { p = p.nextProbablePrime(); synchronized (this) { primes.add(p); } } } public void cancel() { cancelled = true; } public synchronized List } } Листинг 7.1 Использование поля типа volatile для хранения состояния отмены В листинге 7.2 показан пример использования этого класса, позволяющий генератору простых чисел работать в течение одной секунды перед отменой выполнения. Генератор не обязательно остановит работу ровно через одну секунду, так как может возникнуть некоторая задержка между моментом времени запроса на отмену и моментом времени следующей проверки состояния флага, управляющего отменой выполнения цикла. Метод cancel вызывается из блока finally , чтобы гарантированно прервать работу генератора простых чисел, даже в том случае, если вызов метода sleep будет прерван. Если метод cancel не будет вызван, основной поисковый поток будет работать вечно, потребляя циклы ЦП и предотвращая завершение работы JVM. List PrimeGenerator generator = new PrimeGenerator(); new Thread(generator).start(); try { SECONDS.sleep(1); } finally { generator.cancel(); } return generator.get(); } Листинг 7.2 Секундная генерация простых чисел Задача, которая хочет быть отменяемой, должна иметь политику отмены, которая определяет "как”, “когда” и "что" отменять - как другой код может запросить отмену задачи, когда задача проверяет, была ли запрошена отмена, и какие действия задача выполняет в ответ на запрос отмены. Рассмотрим реальный пример остановки платежа по чеку. У банков есть правила, описывающие как подать запрос на остановку платежа, какая оперативность гарантируется при обработке таких запросов и каким процедурам они следует при фактической остановке платежа (например, уведомление другого банка, участвующего в транзакции, и оценка комиссии по счету плательщика). Вместе взятые, эти процедуры и гарантии составляют политику отмены для чека на оплату. Класс PrimeGenerator использует простую политику отмены: клиентский код запрашивает отмену задачи с помощью вызова метода cancel , экземпляр PrimeGenerator проверяет флаг отмены каждый раз, как находит простое число и завершает свою работу, когда обнаруживает, что была запрошена отмена выполнения. 7.1.1 Прерывание Механизм отмены в классе PrimeGenerator в конечном итоге приведет к завершению задачи поиска простого числа, но это может занять некоторое время. Однако, если задача, которая использует этот подход, вызывает блокирующий метод, такой как BlockingQueue.put , у нас может возникнуть более серьезная проблема - задача может никогда не проверить флаг отмены и, следовательно, никогда не завершиться. Класс BrokenPrimeProducer из листинга 7.3 иллюстрирует эту проблему. Поток производителя создаёт простые числа и помещает их в блокирующую очередь. Если производитель опередит потребителя, очередь заполниться и вызов метода put будет заблокирован. Что произойдет, если потребитель попытается отменить задачу производителя, пока заблокирован вызов метода put ? Он может вызвать метод cancel , который в свою очередь установит флагу cancelled значение true , но производитель никогда не проверит состояние флага, потому что он никогда не сможет выйти из заблокированного метода put (потому что потребитель остановил процесс получения простых чисел из очереди). class BrokenPrimeProducer extends Thread { private final BlockingQueue BrokenPrimeProducer(BlockingQueue } public void run() { try { BigInteger p = BigInteger.ONE; while (!cancelled) queue.put(p = p.nextProbablePrime()); } catch (InterruptedException consumed) { } } public void cancel() { cancelled = true; } } void consumePrimes() throws InterruptedException { BlockingQueue BrokenPrimeProducer producer = new BrokenPrimeProducer(primes); producer.start(); try { while (needMorePrimes()) consume(primes.take()); } finally { producer.cancel(); } } Листинг 7.3 Ненадежный механизм отмены, который может оставить задачу производителя застрявшей в заблокированной операции. Не делайте так. Как мы уже упоминали в главе 5, некоторые библиотечные методы блокировки поддерживают прерывание. Прерывание потока является кооперативным механизмом, позволяя одному потоку сигнализировать другому, что он должен, по своему усмотрению и если для него это допустимо, остановить то, что он делает, и сделать что-то другое. В спецификации API или языка нет ничего, что связывало бы прерывание с какой-либо конкретной семантикой механизма отмены, но на практике использование прерывания для чего-либо, кроме механизма отмены, является хрупким и сложным для сопровождения, особенно в более крупных приложениях. Каждый поток имеет статус прерывания (interrupted status) типа boolean ; прерывание потока устанавливает его статус прерывания в true . Класс Thread содержит методы для прерывания потока и запроса статуса прерывания, как показано в листинге 7.4. Метод interrupt прерывает целевой поток, а метод isInterrupted позволяет запросить статус прерывания целевого потока. Плохо именованный статический метод interrupted сбрасывает статус прерывания текущего потока и возвращает его предыдущее значение; это единственный способ сбросить статус прерывания. public class Thread { public void interrupt() { ... } public boolean isInterrupted() { ... } public static boolean interrupted() { ... } } Листинг 7.4 Методы прерывания класса Thread Блокирующие библиотечные методы, такие как Thread.sleep и Object.wait , пытаются определить, когда поток был прерван, и вернуть управление раньше. Они отвечают на прерывание, сбрасывая статус прерывания и бросая исключение InterruptedException , что указывает на то, что блокирующая операция была завершена раньше из-за прерывания. Среда JVM не даёт гарантий, насколько быстро блокирующий метод обнаружит прерывание, но на практике это происходит достаточно быстро. Если поток прерывается, когда он не заблокирован, его статус прерывания устанавливается, и дальнейшее зависит от отменяемой активности, которая должна для обнаружения прерывания опрашивать статус прерывания. Таким образом, прерывание является "липким" - если не будет возбуждено исключение InterruptedException , свидетельство о прерывания сохранится, пока кто-нибудь специально не сбросит статус прерывания. Вызов метода interrupt не обязательно приводит к остановке операции, выполняемой целевым потоком; он просто доставляет сообщение о том, что было запрошено прерывание. Хороший способ думать о прерывании – это представлять себе, что оно фактически не прерывает выполняющийся поток; оно просто запрашивает, чтобы поток прервал сам себя при следующей удобной возможности (эти возможности называются точками отмены (cancellation points)). Некоторые методы, такие как wait , sleep и join воспринимают такие запросы серьезно, бросая исключение, когда они получают запрос на прерывание или сталкиваются с уже установленным состоянием прерывания на входе. Хорошо ведут себя методы, которые могут полностью игнорировать такие запросы, оставляя запросы на прерывание на месте, чтобы вызывающий код мог что-то с ними сделать. Плохо ведут себя методы, “проглатывающие” запросы на прерывание, и тем самым лишающие код, расположенный выше по стеку вызовов, возможности реагировать на них. Статический метод interrupted следует использовать с осторожностью, так как он сбрасывает текущий статус прерывания потока. Если вы вызываете метод interrupted и он возвращает true , в случае если вы не планируете “проглатывать” прерывание, вы должны что-то с этим сделать - либо бросить исключение InterruptedException или восстановить статус прерывания потока, вызвав метод interrupt еще раз, как показано в листинге 5.10. Класс BrokenPrimeProducer иллюстрирует тот момент, что пользовательские механизмы отмены не всегда хорошо взаимодействуют с библиотечными методами блокировки. Если вы кодируете задачи так, чтобы они реагировали на прерывание, вы можете использовать прерывание в качестве механизма отмены и воспользоваться поддержкой прерывания, предоставляемой множеством библиотечных классов. Прерывание, как правило, является наиболее разумным способом реализации механизма отмены. Класс BrokenPrimeProducer можно легко исправить (и упростить), используя прерывание вместо логического флага запроса на отмену, как показано в листинге 7.5. В каждой итерации цикла есть две точки, в которых прерывание может быть обнаружено: в блокирующем вызове метода put и явным опросом статуса прерывания в заголовке цикла. class PrimeProducer extends Thread { private final BlockingQueue PrimeProducer(BlockingQueue } public void run() { try { BigInteger p = BigInteger.ONE; while (!Thread.currentThread().isInterrupted()) queue.put(p = p.nextProbablePrime()); } catch (InterruptedException consumed) { / * Allow thread to exit * / } } public void cancel() { interrupt(); } } Листинг 7.5 Использование прерывания для отмены Явная проверка здесь не является строго необходимой из-за блокирующего вызова put , но она делает класс PrimeProducer более отзывчивым к прерыванию, потому что она проверяет прерывание перед началом выполнения длительной задачи поиска простого числа, а не после. Если вызовы прерываемых блокирующих методов недостаточно часты для обеспечения требуемой скорости отклика, может помочь явная проверка статуса прерывания. 7.1.2 Политики прерывания Так же, как задачи должны иметь политику отмены, также и потоки должны иметь политику прерывания (interruption policy). Политика прерывания определяет, как поток интерпретирует запрос на прерывание - что он делает (если есть что) при обнаружении, какие единицы работы считаются атомарными по отношению к прерыванию и как быстро он реагирует на прерывание. Наиболее разумной политикой прерывания является некоторая форма отмены на уровне потока или на уровне службы: скорейшее завершение работы, очистка ресурсов при необходимости и, возможно, уведомление некоторой сущности- владельца о завершении потока. Можно установить и другие политики прерывания, например приостановку или возобновление работы службы, но потоки или пулы потоков с нестандартными политиками прерывания, возможно, потребуется ограничить задачами, которые были написаны с учетом таких политик. Важно различать, как задачи и потоки должны реагировать на прерывания. Один запрос на прерывание может быть адресован нескольким получателям - прерывание рабочего потока в пуле потоков может означать как “отмена текущей задачи”, так и “завершение рабочего потока”. Задачи не выполняются в собственных потоках; они заимствуют потоки, принадлежащие другим службам, например пулу потоков. Код, не являющийся владельцем потока (для пула потоков таковым является любой код за пределами реализации пула потоков), должен заботиться о сохранении статуса прерывания, чтобы код-владелец мог в конечном итоге обработать прерывание, даже если “гостевой” код также обработает прерывание. (Если вы по чьей-то просьбе ожидаете у кого-то дома, вы не выбрасываете почту, которая приходит, пока хозяев нет дома, - вы сохраняете ее и позволяете им разобраться с ней, когда они возвращаются домой, даже если вы читаете их журналы.) Вот почему большинство блокирующих библиотечных методов в ответ на прерывание просто бросают исключение InterruptedException . Они никогда не будут выполняться в потоке, которым они владеют, поэтому они реализуют наиболее разумную политику отмены для библиотечного кода или кода задач: как можно быстрее отойти в сторону и передать прерывание обратно вызывающему объекту, чтобы код, расположенный выше по стеку вызовов, мог предпринять дальнейшие действия. Нет необходимости в том, чтобы задача обязательно что-нибудь бросала, когда обнаруживает запрос на прерывание - она может отложить это до более благоприятного момента, запомнив, что она была прервана, дождаться завершения операции, которую она выполняла, а затем бросив исключение InterruptedException или иным образом указав на прерывание. Этот метод может защитить структуры данных от повреждения, когда выполнение активности прерывается в середине операции обновления. Задача не должна делать предположений о политике прерывания выполняющегося потока, только если она явно не предназначена для выполнения в службе, имеющей определенную политику прерывания. Независимо от того, интерпретирует ли задача прерывание как отмену или выполняет какое-либо другое действие при прерывании, она должна позаботиться о сохранении состояния прерывания выполняющегося потока. Если она не сможет распространить исключение InterruptedException до вызывающего объекта, она должна будет восстановить статус прерывания после перехвата исключения InterruptedException : Thread.currentThread().interrupt(); Так же, как код задачи не должен делать предположений о том, что прерывание значит для выполняющего его потока, код отмены не должен делать предположений о политике прерывания произвольных потоков. Поток должен прерываться только своим владельцем; владелец может инкапсулировать знание о политике прерывания потока в соответствующем механизме отмены, таком как метод завершения работы. Поскольку каждый поток имеет свою собственную политику прерывания, не следует прерывать поток, если вы не знаете, что прерывание означает для этого потока. Критики высмеивали средства прерывания в Java, потому что они не предоставляют возможность упреждающего прерывания, но, в то же время, заставляет разработчиков обрабатывать исключение InterruptedException Однако возможность отложить запрос на прерывание, позволяет разработчикам создавать гибкие политики прерывания, балансирующие отзывчивость и надежность в зависимости от требований приложения. |