关闭线程时发生死锁

发布于 2024-07-26 08:12:57 字数 3048 浏览 2 评论 0原文

我创建了一个类,用于打开 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() 方法离开时没有读/写线程处于活动状态,这意味着我不能只设置这些线程的 Termerated 标志并离开。

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

感谢您提前的建议。

马吕斯。

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

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)时,应在该线程中引发异常。 我能否确定,如果我在该线程中捕获该异常,它不会影响整个应用程序的工作? 这个问题可能听起来很愚蠢,但我认为某些异常可能会导致操作系统关闭导致它的应用程序,对吗? 在这种情况下我需要担心这个吗?

I created a class that opens a COM port and handles overlapped read and write operations. It contains two independent threads - one that reads and one that writes data. Both of them call OnXXX procedures (eg OnRead or OnWrite) notifying about finished read or write operation.

The following is a short example of the idea how the threads work:

  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;

When you look at the Close() procedure, you will see that it enters the critical section, terminates the writing thread and then waits for it to finish.
Because of the fact that the writing thread can enqueue a new value to be written when it calls the OnWrite method, it will try to enter the same critical section when calling the Write() procedure of the TAsyncSerialPort class.

And here we've got a deadlock. The thread that called the Close() method entered the critical section and then waits for the writing thread to be closed, while at the same time that thread waits for the critical section to be freed.

I've been thinking for quite a long time and I didn't manage to find a solution to that problem. The thing is that I would like to be sure that no reading/writing thread is alive when the Close() method is left, which means that I cannot just set the Terminated flag of those threads and leave.

How can I solve the problem? Maybe I should change my approach to handling serial port asynchronously?

Thanks for your advice in advance.

Mariusz.

--------- EDIT ----------
How about such a solution?

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;

If my thinking is correct, this should eliminate the deadlock problem. Unfortunately, however, I close the comm port handle before the writing thread is closed. This means that when it calls any method that takes the comm port handle as one of its arguments (eg Write, Read, WaitCommEvent) an exception should be raised in that thread. Can I be sure that if I catch that exception in that thread it will not affect the work of the whole application? This question may sound stupid, but I think some exceptions may cause the OS to close the application that caused it, right? Do I have to worry about that in this case?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

〆凄凉。 2024-08-02 08:12:57

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

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

Yes, you should probably reconsider your approach. Asynchronous operations are available exactly to eliminate the need for threads. If you use threads, then use synchronous (blocking) calls. If you use asynchronous operations, then handle everything in one thread - not necessarily the main thread, but it doesn't make sense IMO to do the sending and receiving in different threads.

There are of course ways around your synchronization problem, but I'd rather change the design.

世俗缘 2024-08-02 08:12:57

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

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

编辑:

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

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

You can take the lock out of the Close. By the time it returns from the WaitFor, the thread body has noticed it has been terminated, completed the last loop, and ended.

If you don't feel happy doing this, then you could move setting the lock just before the FreeAndNil. This explicitly lets the thread shutdown mechanisms work before you apply the lock (so it won't have to compete with anything for the lock)

EDIT:

(1) If you also want to close the comms handle do it after the loop in the Execute, or in the thread's destructor.

(2) Sorry, but your edited solution is a terrible mess. Terminate and Waitfor will do everything you need, perfectly safely.

予囚 2024-08-02 08:12:57

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

The main problem seems to be that you place the entire content of Close in a critical section. I'm almost sure (but you'll have to check the docs) that TThread.Terminate and TThread.WaitFor are safe to call from outside the section. By pulling that part outside the critical section you will solve the deadlock.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文