Multithread - Delphi - Потоки

Главная   1 глава   2 глава   3 глава   4 глава   5 глава   6 глава   7 глава   8 глава   9 глава  

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

Содержание:


* Метод WaitFor.


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


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


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


* Вы заметили ошибку?


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

Метод WaitFor.

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


* Главному потоку VCL нужен доступ к объекту рабочего потока после его остановки для чтения или записи содержащихся в нем данных .


* Принудительное завершение потоков при закрытии программы неприемлемо.

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

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

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

Выделить всёРазвернуть кодСвернуть кодкод Pascal/Delphi

unit PrimeForm;

interface

uses

Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,

StdCtrls, PrimeThread;

const

WM_THREAD_COMPLETE = WM_APP + 5437; { Just a magic number }

type

TPrimeFrm = class(TForm)

NumEdit: TEdit;

SpawnButton: TButton;

ResultsMemo: TMemo;

procedure SpawnButtonClick(Sender: TObject);

procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);

private

{ Private declarations }

FThread: TPrimeThrd;

procedure HandleThreadCompletion(var Message: TMessage); message WM_THREAD_COMPLETE;

public

{ Public declarations }

end;

var

PrimeFrm: TPrimeFrm;

implementation

{$R *.DFM}

procedure TPrimeFrm.HandleThreadCompletion(var Message: TMessage);

begin

if Assigned(FThread) then

begin

FThread.WaitFor;

FThread.Free;

FThread := nil;

end;

end;

procedure TPrimeFrm.SpawnButtonClick(Sender: TObject);

begin

if not Assigned(FThread) then

begin

FThread := TPrimeThrd.Create(True);

FThread.FreeOnTerminate := false;

try

with FThread do

begin

TestNumber := StrToInt(NumEdit.Text);

Resume;

end;

except on EConvertError do

begin

FThread.Free;

FThread := nil;

ShowMessage('That is not a valid number!');

end;

end;

end;

end;

procedure TPrimeFrm.FormCloseQuery(Sender: TObject; var CanClose: Boolean);

begin

CanClose := true;

if Assigned(FThread) then

begin

if MessageDlg('Threads active. Do you still want to quit?',

mtWarning, [mbYes, mbNo], 0) = mrNo then

CanClose := false;

end;

{Sleep(50000);}{Line C}

if CanClose then

begin

if Assigned(FThread) then

begin

FThread.Terminate;

FThread.WaitFor;

FThread.Free;

FThread := nil;

end;

end;

end;

end.

Можно увидеть несколько отличий от предыдущего примера:


* В начале модуля объявлено "магическое число" . Это относительный номер сообщения, а значение его не важно, главное, чтобы оно было уникальным.


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


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


* Код создания потока не устанавливает свойство FreeOnTerminate в True, вместо этого основной поток VCL будет освобождать рабочий поток позже.


* У главной формы есть обработчик сообщения, который ждет завершения рабочего потока, и затем разрушает его.


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

Учтите эти пункты, а здесь код рабочего потока

Выделить всёРазвернуть кодСвернуть кодкод Pascal/Delphi

unit PrimeThread;

interface

uses

Classes;

type

TPrimeThrd = class(TThread)

private

FTestNumber: integer;

FResultString: string;

protected

function IsPrime: boolean;

procedure UpdateResults;

procedure Execute; override;

public

property TestNumber: integer write FTestNumber;

end;

implementation

uses SysUtils, Dialogs, PrimeForm, Windows;

procedure TPrimeThrd.UpdateResults;

begin

PrimeFrm.ResultsMemo.Lines.Add(FResultString);

end;

function TPrimeThrd.IsPrime: boolean;

var

iter: integer;

begin

result := true;

if FTestNumber < 0 then

begin

result := false;

exit;

end;

if FTestNumber <= 2 then

exit;

iter := 2;

while (iter < FTestNumber) and (not terminated) do {Line A}

begin

if (FTestNumber mod iter) = 0 then

begin

result := false;

{exit;}

end;

Inc(iter);

end;

end;

procedure TPrimeThrd.Execute;

begin

if IsPrime then

FResultString := IntToStr(FTestNumber) + ' is prime.'

else

FResultString := IntToStr(FTestNumber) + ' is not prime.';

if not Terminated then {Line B}

begin

Synchronize(UpdateResults);

PostMessage(PrimeFrm.Handle, WM_THREAD_COMPLETE, 0, 0);

end;

end;

end.

В нем тоже есть небольшие отличия от того, что было в Главе 3:


* Функция IsPrime теперь проверяет запросы на завершение потока, обеспечивая быстрый выход, если установлено свойство Terminated.


* Процедура Execute делает проверку на нештатное завершение.


* При нормальном завершении используется Synchronize для показа результатов и главной форме посылается сообщение - запрос на освобождение потока.

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

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

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

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

--Resize_Images_Alt_Text--

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

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

--Resize_Images_Alt_Text--

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

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 предусмотрели перехват такой ошибки: в рабочем потоке возбуждается исключение, таким образом цикл прерывается, и поток завершается.

--Resize_Images_Alt_Text--

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

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

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


* Удалить "and not terminated" в строке Line A


* Заменить "not terminated" в строке Line B на "true".


* Удалить комментарий для строки Line C.

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


* Пользователь запускает вычислительный поток.


* Пользователь пытается выйти и говорит "Да, я хочу выйти, несмотря на тот факт, что потоки еще работают".


* Главный поток приложения засыпает (строка C)


* Вычислительный поток попадает в конец исполняемого кода и вызывает Synchronize. (Достигается модификацией строк A и B).


* Главный поток приложения просыпается и вызывает WaitFor.

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

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


* Удаление метода потока "DisplayResults".


* Добавление подходящего свойства рабочего потока.


* Изменение обработчика в главной форме.

Изменения

Выделить всёРазвернуть кодСвернуть кодкод Pascal/Delphi

{ Unit PrimeThread }

type

TPrimeThrd = class(TThread)

private

FTestNumber: integer;

FResultString: string;

protected

function IsPrime: boolean;

procedure Execute; override;

public

property TestNumber: integer write FTestNumber;

property ResultString: string read FResultString;

end;

procedure TPrimeThrd.Execute;

begin

if IsPrime then

FResultString := IntToStr(FTestNumber) + ' is prime.'

else

FResultString := IntToStr(FTestNumber) + ' is not prime.';

if not Terminated then {Line B}

PostMessage(PrimeFrm.Handle, WM_THREAD_COMPLETE, 0, 0);

end;

{ Unit PrimeForm }

procedure TPrimeFrm.HandleThreadCompletion(var Message: TMessage);

begin

if Assigned(FThread) then

begin

FThread.WaitFor;

ResultsMemo.Lines.Add(FThread.ResultString);

FThread.Free;

FThread := nil;

end;

end;

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







© Copyright by Tregert, 2010
Сайт управляется системой uCoz