Глава 5. Снова о разрушении потока. Тупик или зацикливание (Deadlock).

Содержание:

Метод WaitFor.

OnTerminate, как обсуждалось в предыдущей части, полезно, если вы используете поток в режиме "выполнить и забыть", с автоматическим разрушением. Но что, если в некий момент выполнения главного потока VCL вы должны быть уверены, что все остальные потоки завершены? Решение состоит в использовании метода WaitFor, который пригодится в следующих случаях:

Попросту говоря, когда поток А вызывает метод WaitFor потока B, сам он приостановливается, пока поток B не завершится. И когда поток А продолжит свое выполнение, можно быть уверенным, что результаты из потока B можно прочесть, и что объект потока B можно уничтожать. Обычно при завершении программы основной поток VCL вызывает Terminate всех вторичных потоков, и затем ожидает их завершения (WaitFor), после чего осуществляется выход из программы.

Контролируемое завершение потока - Подход 2.

В этом примере мы модифицируем код программы для простых чисел так, чтобы в каждый момент выполнялся только один поток, и программа перед выходом будет ждать завершения потока. Хотя в этой программе и не обязательно основному потоку ждать завершения других, но упражнение будет полезным и продемонстрирует несколько свойств WaitFor, которые не всегда желательны, а также проиллюстрирует пару довольно тонких моментов, которые могут быть упущены новичками в программировании потоков. Сначала код главной формы. Можно увидеть несколько отличий от предыдущего примера:

Учтите эти пункты, а здесь код рабочего потока. В нем тоже есть небольшие отличия от того, что было в Главе 3:

Введение в обработку сообщений и отложенное уведомление

При нормальном ходе дел поток выполняется, использует Synchronize для показа результатов, а затем посылает сообщение главной форме. Это сообщение асинхронно: главная форма получит его в некоторый момент чуть позже. PostMessage не приостанавливает рабочий поток, он продолжает свою работу до завершения. Это очень полезное свойство: мы не можем использовать Synchronize для того, чтобы сообщить главной форме, что пора освободить поток, потому что тогда мы бы "вернулись" из вызова Synchronize в несуществующий уже поток. Вместо этого, происходит лишь уведомление (notification), вежливое напоминание главной форме о том, что следует освободить поток при первой возможности.

В некоторый момент главный программный поток получает сообщение и выполняется его обработчик. Этот обработчик проверяет, существует ли поток, если существует, ждет, пока он закончит выполняться. Этот шаг необходим, поскольку, хотя рабочий поток и близок к завершению (после PostMessage операторов мало), но гарантии нет. Когда ожидание закончено, главный поток очищает рабочий поток.

Диаграмма иллюстрирует первый случай. Ради упрощения детали Synchronize на диаграмме не приводятся. Кроме того, вызов PostMessage показан как происходящий несколько раньше окончания кода рабочего потока, чтобы продемонстрировать функционирование WaitFor.

Позднее мы покажем преимущества посылки сообщений более детально. Сейчас достаточно сказать, что эта техника полезна при взаимодействии с главным потоком VCL.

В случае принудительного завершения пользователь пытается выйти из программы и подтверждает, чтo хочет это сделать немедленно. Главный поток устанавливает свойство Terminated рабочего потока, что приводит к довольно быстрой его остановке, и затем ждет его завершения. После завершения, как и в предыдущем случае, производится очистка. Диаграмма иллюстрирует второй случай.

Многие читатели в настоящий момент могут полностью удовлетвориться такой ситуацией. Однако подводные камни остаются, и, как часто бывает при рассмотрении многопоточной синхронизации, они проявляются в деталях.

WaitFor может вызвать долгую задержку.

Преимущество метода WaitFor является также и его крупнейшим недостатком: он переводит главный поток в состояние, в котором тот не может принимать сообщения. Это означает, что программа не может предпринять никаких операций, связанных обычно с обработкой сообщений: в состоянии такого ожидания приложение не будет перерисовываться, изменять размер формы или отвечать на внешние воздействия. Когда пользователь это заметит, он решит, что программ зависла. Это не беда в случае нормального завершения потока; вызывая PostMessage самым последним оператором рабочего потока, мы гарантируем, что главному потоку не придется долго ждать. В случае же нештатного завершения потока время, потраченное главным потоком на ожидание, зависит в основном от того, как часто рабочий поток проверяет свое свойство Terminate. Код PrimeThread содержит строку, помеченную "Line A". Если удалить "and not terminated", то вы можете поэкспериментировать с выходом из приложения во время исполнения длительного потока.

Существует несколько способов разрешения этой дилеммы с использованием функций ожидания сообщений Win32, а объяснение таких методов можно найти, посетив http://www.midnightbeach.com/jon/pubs/MsgWaits/MsgWaits.html. В целом же проще писать потоки, которые регулярно проверяют свойство Terminated. Если это невозможно, тогда лучше выдавать пользователю предупреждение о потенциальной невосприимчивости в течение некоторого времени (подобно Microsoft Exchange.)

Вы заметили ошибку? WaitFor и Synchronize: зацикливание.

Задержка, вызываемая WaitFor - незначительная проблема по сравнению с другой. В приложениях которые используют как Synchronize, так и WaitFor, вполне возможно вызвать зависание, зацикливание приложения (deadlock, тупик). Тупиком можно считать случай, когда в приложении нет алгоритмических ошибок, но оно заторможено, не отзывается на действия пользователя. Обычно оно происходит, если потоки циклически ожидают друг друга. Поток А может ждать завершения потоком B некоторой операции, в то время как поток C ждет поток D, и т.д.. А вот поток D может ждать завершения некоторых действий потоком А. К сожалению поток А не может завершить операцию, поскольку он приостановлен. Это программный эквивалент проблемы "A: Сначала проезжайте Вы... B: Нет, Вы... A: Нет, я настаиваю!", которая приводит к автомобильным пробкам, когда примущественное право проезда не очевидно. Это поведение документировано и в файлах помощи VCL.

В этом конкретном случае зацикливание может произойти для двух потоков, если вычислительный поток вызывает Synchronize прямо перед тем,как основной поток вызывает WaitFor. Тогда вычислительный поток будет ждать, пока основной поток не вернется в цикл обработки сообщений, а основной будет ждать завершения вычислительного. Произойдет зацикливание. Возможно также, что основной поток VCL вызовет WaitFor незадолго до вызова Synchronize рабочим потоком. Это тоже может привести к зацикливанию. К счастью, разработчики VCL предусмотрели перехват такой ошибки: в рабочем потоке возбуждается исключение, таким образом цикл прерывается, и поток завершается.

Реализация примера делает это маловероятным. Рабочий поток вызывает Synchronize только при чтении свойства Terminated, если оно установлено в False, незадолго до окончания выполнения. Основной поток приложения устанавливает свойство Terminated прямо перед вызовом WaitFor. Таким образом, для того, чтобы произошло зацикливание, рабочий поток должен был бы определить, что свойство Terminated=False, выполнить Synchronize, и затем управление должно быть передано в основной поток точно в тот момент, когда пользователь подтвердил принудительный выход из программы.

Несмотря на то, что в этом случае зацикливание маловероятно, события подобного типа явно могут привести к конфликтам. Все зависит от точных временных интервалов между событиями, которые могут меняться от запуска к запуску и от и от машины к машине. В 99.9% случаев принудительное закрытие сработает, но один раз из тысячи все может заблокироваться: этой проблемы следует избегать во что бы то ни стало. Читатель может вспомнить, что я прежде упоминал, что никакой серьезной синхронизации не происходит при чтении или записи свойства Terminated. Это означает, что невозможно использовать свойство Terminated для полного исключения указанной проблемы, как и доказывает предыдущая диаграмма.

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

Теперь можно спровоцировать зацикливание, запустив поток, чье выполнение займет порядка 20 секунд, и попытавшись выйти из приложения сразу после создания потока. Вы можете также захотеть подобрать временной интервал, в течение которого главный поток приложения "засыпает", чтобы добиться "правильного" порядка событий:

Как избежать такого тупика.

Наилучший метод не допускать этой формы зацикливания - не использовать WaitFor и Synchronize в одном приложении. От WaitFor можно избавиться, применяя событие OnTerminate, как обсуждалось выше. Данный пример оказался довольно удачен в этом отношении, поскольку возвращаемые потоком результаты очень просты, так что мы можем избежать использования Synchronize. Используя WaitFor, основной поток может легально иметь доступ к свойствам рабочего потока после его завершения, и все, что нам нужно - переменная "result" для хранения текстовой строки, полученной в рабочем потоке. Необходимые модификации:

 

Изменения. Обсуждение механизмов синхронизации, общих для всех 32-битовых версий Delphi, почти закончено. Я еще не рассмотрел методы TThread.Suspend и TThread.Resume. Они будут обсуждаться в Главе 10. В дальнейших частях исследуются средства, предоставляемые Win32 API и последними версиями Delphi. Я хотел бы предложить, чтобы, как только читатель освоится с основами работы с потоками в Delphi, он нашел время для изучения этих более продвинутых методов, поскольку они намного более гибкие, чем встроенные в Delphi, и позволяют программисту согласовывать работу потоков изящнее и эффективнее, а также уменьшают возможность написания кода, ведущего к зацикливанию.


[Содержание] [Назад][Вперед]

© Martin Harvey 2000.
 

Hosted by uCoz