Вверх ↑
Ответов: 4621
Рейтинг: 746
#1: 2017-05-15 12:04:23 ЛС | профиль | цитата
Galkov писал(а):
зачем у тебя OvrWr.hEvent создан с ручным сбросом
Это как раз нормально - его будет сигналить overlapped-операция (WaitCommEvent). Хотя, не так: в данном случае это просто не имеет значения.
Galkov писал(а):
и в исходно-сигнальном состоянии
Тут смысла нет, возможно перед началом операции оно все равно сбрасывается, поэтому работало.



Продолжим разговор.
Поскольку глядя на коды KOL.TThread (кстати, вот они, чтобы далеко не ходить, толькое основное):
KOL.TThread


TThread = class(TObj)
public
destructor Destroy; override;

function Execute: integer; virtual;

procedure Resume;

procedure Suspend;
procedure Terminate;

function WaitFor: Integer;
property Handle: THandle read FHandle;
property Suspended: boolean read FSuspended;
property Terminated: boolean read FTerminated;
property ThreadId: DWORD read FThreadId;
property PriorityClass: Integer read GetPriorityCls write SetPriorityCls;
property ThreadPriority: Integer read GetThrdPriority write SetThrdPriority;
property Data : Pointer read FData write FData;
property OnExecute: TOnThreadExecute read FOnExecute write FOnExecute;
property OnSuspend: TObjectMethod read FOnSuspend write FOnSuspend;
property OnResume: TOnEvent read FOnResume write FOnResume;
procedure Synchronize( Method: TThreadMethod );
procedure SynchronizeEx( Method: TThreadMethodEx; Param: Pointer );
property AutoFree: Boolean read F_AutoFree write F_AutoFree;
end;

destructor TThread.Destroy;
begin
if not FTerminated then
begin
Terminate;
WaitFor;
end;
if (FHandle <> INVALID_HANDLE_VALUE) then
CloseHandle(FHandle);
inherited;
end;

function TThread.Execute: integer;
begin
Result := 0;
if Assigned( FOnExecute ) then
Result := FOnExecute( Self );
if F_AutoFree then
begin
FTerminated := TRUE;
Free;
end;
end;

procedure TThread.Terminate;
begin
TerminateThread(FHandle,0);
FTerminated := True;
end;

function TThread.WaitFor: Integer;
begin
RefInc;
Result := -1;
if FHandle = 0 then Exit;
WaitForSingleObject(FHandle, INFINITE);
GetExitCodeThread(FHandle, DWORD(Result));
RefDec;
end;
единственным способом установить Terminated=True является вызов Terminate, то вот это while not Sender.Terminated do не имеет в данной реализации никакого смысла - когда Terminated станет True этот цикл уже будет грубо прекращен. Особенно печально выглядит ситуация, когда COMEX.doClose будет вызвано из, например, COMEX.onRead. Тогда в вышеприведеной THICOMEX.CloseCom всё, что после thrd.Terminate() вообще не выполнится, в том числе free_and_nil(thrd), free_and_nil(thwr), CloseHandle(hFile).

Отступление.
Не знаю чем руководствовался Кладов, когда добавлял TerminateThread() в TThread.Terminate. Возможно, чтобы название метода соответствовало его назначению. Но в оригинальном Delphi Classes.TThread метод Terminate делает только установку свойства Terminated, что даёт некоторый смысл коду компонента COMEX, особенно учитывая, что nesco при написании этого компонента смотрел примеры как раз, вероятней всего, для Delphi.

Поскольку пользователи не имеют возможности регулярно обновлять у себя компиляторы, то мы избегаем вносить изменения в KOL.pas. Поэтому все дальнейшие обсуждения не будут предполагать правки KOL.TThread, вместо этого последним этапом обсуждения будет выработка альтернативного решения.

--- Добавлено в 2017-05-15 12:08:19

Итак, как выше написал Galkov, первый вывод: Нельзя использовать метод TThread.Terminate(). Просто забудьте о его существовании.

А как же тогда останавливать поток? Первый вариант - добавить в класс THICOMEX поле FStopReading: Boolean. Тогда код может выглядеть так:


function THICOMEX.CloseCom;
begin
result := false;
if Assigned(thrd) then
begin
FStopReading := True; // <<<<<<<<<<<<<<<
thrd.WaitFor;
free_and_nil(thrd);
end;
......................
end;

function THICOMEX.ExecuteRd;
......................
begin
while not FStopReading do // <<<<<<<<<<<<<<<
begin
......................
end;
......................
end;
У этого варианта есть недостатки. Во-первых, такое нужно делать всегда, когда мы используем класс TThread, то есть, лишняя работа. Во-вторых, этих флагов нужно добавить столько, сколько потоков мы используем. Например, в THICOMEX нужно добавить ещё и флаг для остановки потока записи (вместо thwr.Terminate).
Предложение такое: а давайте в наш класс TThread добавим свойство Stopped: Boolean и метод TThread.Stop() который выставляет Stopped = True.
Тогда код будет выглядеть так:


function THICOMEX.CloseCom;
begin
result := false;
if Assigned(thrd) then
begin
thrd.Stop; // <<<<<<<<<<<<<<<
thrd.WaitFor;
free_and_nil(thrd);
end;
......................
end;

function THICOMEX.ExecuteRd;
......................
begin
while not Sender.Stopped do // <<<<<<<<<<<<<<<
begin
......................
end;
......................
end;
Просто и понятно.

--- Добавлено в 2017-05-15 12:09:54

Продолжим дальше. Метод TThread.WaitFor приостанавливает вызваший его поток, пока поток в TThread не завершит свое исполнение, то есть это ожидание завершения параллельного потока. В текущей реализации при thrd.Terminate дальнейший вызов thrd.WaitFor особого смысла не имел - поток и так будет убит. Но, когда мы вместо thrd.Terminate делаем thrd.Stop, поток (метод THICOMEX.ExecuteRd) не убивается мгновенно - он продолжает стоять на таймаутах и выполнять другие операторы до тех пор, пока не дойдёт в начало цикла, где стоит проверка while not Sender.Stopped do.
Если после thrd.Stop вызвать сразу free_and_nil(thrd), то объект thrd (он же Sender) будет освобожден, и тогда параллельный поток упадёт на проверке while not Sender.Stopped do, потому что объекта Sender уже не существует. По крайней мере, так должен размышлять автор кода, и первое, что ему должно придти в голову - это дождаться завершения метода THICOMEX.ExecuteRd и параллельного потока, для чего он и использовал метод WaitFor.
А тепер представьте, что пользователь решил закрыть COM порт и вызвал COMEX.doClose (и следовательно, THICOMEX.CloseCom) из события COMEX.onRead.
Что произойдет в этом случае? А в этом случае параллельный поток в особе метода THICOMEX.ExecuteRd выполнит команду thrd.WaitFor и... и будет вечно ожидать завершения самого себя. Это назвыется deadlock. А если вызвать COMEX.doClose из синхронного метода COMEX.onSyncRead, то повиснет ещё и главный поток приложения.

Отступление.
Почему я написал "По крайней мере, так должен размышлять автор кода"? Потому что Кладов что-то подобное предполагал, и на самом деле метод TObj.Destroy (который вызывается по free_and_nil(thrd)) не уничтожает объект, а уменьшает специальный счетчик, и когда этот счетчик дойдёт до 0, только тогда объект будет уничтожен. А у TThread перед запуском параллельного потока на исполнение счетчик как раз увеличивается. Идея важная и я к ней ещё вернусь. Но в KOL её реализация, по-моему, бажная, и нам KOL-овскую реализацию всё равно использовать неудобно, потому что у нас классы, а не объекты и нет наследования KOL-овских объектов, поэтому всё равно нужно делать свою реализацию.

Как мы видим, метод WaitFor мы не можем использовать, даже если обойдём deadlock (это возможно).
Тогда вопрос: как нам следует освободить объект thrd в методе THICOMEX.CloseCom после метода thrd.Stop?
карма: 26

0
Редактировалось 13 раз(а), последний 2017-05-15 14:37:26