1

我创建了一个类,它打开一个 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 方法时可以将要写入的新值排入队列,因此在调用 TAsyncSerialPort 类的 Write() 过程时,它会尝试进入相同的临界区。

在这里,我们陷入了僵局。调用 Close() 方法的线程进入临界区,然后等待写入线程关闭,同时该线程等待释放临界区。

我已经思考了很长时间,但我没有设法找到解决这个问题的方法。问题是我想确保当 Close() 方法离开时没有读/写线程处于活动状态,这意味着我不能只设置这些线程的 Terminated 标志并离开。

我该如何解决这个问题?也许我应该改变异步处理串行端口的方法?

提前感谢您的建议。

马吕斯。

--------- 编辑 ----------
这样的解决方案怎么样?

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;

如果我的想法是正确的,这应该可以消除死锁问题。然而不幸的是,我在写线程关闭之前关闭了通信端口句柄。这意味着当它调用任何将通信端口句柄作为其参数之一的方法(例如,Write、Read、WaitCommEvent)时,应该在该线程中引发异常。我可以确定如果我在该线程中捕获该异常,它不会影响整个应用程序的工作吗?这个问题听起来可能很愚蠢,但我认为某些异常可能会导致操作系统关闭导致它的应用程序,对吧?在这种情况下我需要担心吗?

4

3 回答 3

6

是的,您可能应该重新考虑您的方法。异步操作完全可以用来消除对线程的需求。如果您使用线程,则使用同步(阻塞)调用。如果您使用异步操作,则在一个线程中处理所有内容 - 不一定是主线程,但 IMO 在不同线程中进行发送和接收是没有意义的。

当然有解决同步问题的方法,但我宁愿改变设计。

于 2009-07-05T09:46:12.833 回答
4

您可以将锁从关闭状态中取出。当它从 WaitFor 返回时,线程主体已经注意到它已经终止,完成了最后一个循环,并结束了。

如果您不喜欢这样做,那么您可以在 FreeAndNil 之前设置锁。这明确地让线程关闭机制在您应用锁之前工作(因此它不必与任何东西竞争锁)

编辑:

(1) 如果您还想关闭通讯句柄,请在 Execute 或线程的析构函数中的循环之后执行。

(2) 抱歉,您编辑的解决方案一团糟。Terminate 和 Waitfor 将完全安全地完成您需要的一切。

于 2009-07-05T09:58:07.397 回答
2

主要问题似乎是您将 Close 的全部内容放在关键部分中。我几乎可以肯定(但您必须查看文档) TThread.Terminate 和 TThread.WaitFor 从该部分外部调用是安全的。通过将该部分拉到关键部分之外,您将解决死锁。

于 2009-07-05T09:41:15.997 回答