Взаимоблокировка при закрытии потока
-
23-08-2019 - |
Вопрос
Я создал класс, который открывает COM-порт и обрабатывает перекрывающиеся операции чтения и записи.Он содержит два независимых потока - один, который считывает, и другой, который записывает данные.Оба они вызывают процедуры OnXXX (например, OnRead или onWrite), уведомляющие о завершенной операции чтения или записи.
Ниже приведен краткий пример идеи о том, как работают потоки:
TOnWrite = procedure (Text: string);
TWritingThread = class(TThread)
strict private
FOnWrite: TOnWrite;
FWriteQueue: array of string;
FSerialPort: TAsyncSerialPort;
protected
procedure Execute; override;
public
procedure Enqueue(Text: string);
{...}
end;
TAsyncSerialPort = class
private
FCommPort: THandle;
FWritingThread: TWritingThread;
FLock: TCriticalSection;
{...}
public
procedure Open();
procedure Write(Text: string);
procedure Close();
{...}
end;
var
AsyncSerialPort: TAsyncSerialPort;
implementation
{$R *.dfm}
procedure OnWrite(Text: string);
begin
{...}
if {...} then
AsyncSerialPort.Write('something');
{...}
end;
{ TAsyncSerialPort }
procedure TAsyncSerialPort.Close;
begin
FLock.Enter;
try
FWritingThread.Terminate;
if FWritingThread.Suspended then
FWritingThread.Resume;
FWritingThread.WaitFor;
FreeAndNil(FWritingThread);
CloseHandle(FCommPort);
FCommPort := 0;
finally
FLock.Leave;
end;
end;
procedure TAsyncSerialPort.Open;
begin
FLock.Enter;
try
{open comm port}
{create writing thread}
finally
FLock.Leave;
end;
end;
procedure TAsyncSerialPort.Write(Text: string);
begin
FLock.Enter;
try
{add Text to the FWritingThread's queue}
FWritingThread.Enqueue(Text);
finally
FLock.Leave;
end;
end;
{ TWritingThread }
procedure TWritingThread.Execute;
begin
while not Terminated do
begin
{GetMessage() - wait for a message informing about a new value in the queue}
{pop a value from the queue}
{write the value}
{call OnWrite method}
end;
end;
Когда вы посмотрите на процедуру Close(), вы увидите, что она входит в критическую секцию, завершает поток записи, а затем ожидает его завершения.Из-за того факта, что поток записи может ставить в очередь новое значение для записи при вызове метода onWrite, он попытается войти в ту же критическую секцию при вызове процедуры Write() класса TAsyncSerialPort.
И здесь мы зашли в тупик.Поток, вызвавший метод Close(), вошел в критическую секцию, а затем ожидает закрытия потока записи, в то же время этот поток ожидает освобождения критической секции.
Я довольно долго думал, и мне так и не удалось найти решение этой проблемы.Дело в том, что я хотел бы быть уверен, что ни один поток чтения / записи не активен, когда метод Close() оставлен, что означает, что я не могу просто установить флаг Terminated для этих потоков и уйти.
Как я могу решить эту проблему?Может быть, мне следует изменить свой подход к асинхронной обработке последовательного порта?
Заранее благодарю за ваш совет.
Mariusz.
--------- РЕДАКТИРОВАТЬ ----------
Как насчет такого решения?
procedure TAsyncSerialPort.Close;
var
lThread: TThread;
begin
FLock.Enter;
try
lThread := FWritingThread;
if Assigned(lThread) then
begin
lThread.Terminate;
if lThread.Suspended then
lThread.Resume;
FWritingThread := nil;
end;
if FCommPort <> 0 then
begin
CloseHandle(FCommPort);
FCommPort := 0;
end;
finally
FLock.Leave;
end;
if Assigned(lThread) then
begin
lThread.WaitFor;
lThread.Free;
end;
end;
Если мое мышление верно, это должно устранить проблему взаимоблокировки.К сожалению, однако, я закрываю дескриптор порта связи до того, как поток записи будет закрыт.Это означает, что когда он вызывает любой метод, который принимает дескриптор comm-порта в качестве одного из своих аргументов (например, Write, Read, WaitCommEvent), в этом потоке должно быть вызвано исключение.Могу ли я быть уверен, что если я поймаю это исключение в этом потоке, это не повлияет на работу всего приложения?Этот вопрос может показаться глупым, но я думаю, что некоторые исключения могут привести к тому, что ОС закроет приложение, которое вызвало это, верно?Должен ли я беспокоиться об этом в данном случае?
Решение
Да, вам, вероятно, следует пересмотреть свой подход.Асинхронные операции доступны именно для того, чтобы исключить необходимость в потоках.Если вы используете потоки, то используйте синхронные (блокирующие) вызовы.Если вы используете асинхронные операции, то обрабатывайте все в одном потоке - не обязательно в основном потоке, но IMO не имеет смысла выполнять отправку и получение в разных потоках.
Конечно, есть способы обойти вашу проблему синхронизации, но я бы предпочел изменить дизайн.
Другие советы
Вы можете снять блокировку с Закрытия.К тому времени, когда он возвращается из waitFor, тело потока замечает, что оно было прервано, завершает последний цикл и завершается.
Если вы не чувствуете себя счастливым, делая это, то вы могли бы перенести установку блокировки непосредственно перед запуском FreeAndNil.Это явно позволяет механизмам завершения потока работать до того, как вы примените блокировку (так что ему не придется ни с чем конкурировать за блокировку).
Редактировать:
(1) Если вы также хотите закрыть дескриптор связи, сделайте это после цикла при выполнении или в деструкторе потока.
(2) Извините, но ваше отредактированное решение представляет собой ужасный беспорядок.Завершите работу и Waitfor сделает все, что вам нужно, совершенно безопасно.
Основная проблема, по-видимому, заключается в том, что вы помещаете все содержимое Close в критический раздел.Я почти уверен (но вам придется проверить документы), что TThread.Terminate и TThread.waitFor безопасны для вызова из-за пределов раздела.Вытащив эту часть за пределы критической секции, вы разрешите тупиковую ситуацию.