Вопрос

Я создал класс, который открывает 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 безопасны для вызова из-за пределов раздела.Вытащив эту часть за пределы критической секции, вы разрешите тупиковую ситуацию.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top