【问题标题】:Ensure all TThread.Queue methods complete before thread self-destructs确保所有 TThread.Queue 方法在线程自毁之前完成
【发布时间】:2014-04-29 08:34:59
【问题描述】:

我发现如果以TThread.Queue 排队的方法调用调用TApplication.WndProc 的方法(例如ShowMessage),则允许在原始方法完成之前运行后续排队的方法。更糟糕的是,它们似乎没有按 FIFO 顺序调用。

[编辑:实际上它们确实以 FIFO 顺序开始。使用ShowMessage,看起来后面的会先运行,因为在对话框出现之前调用了CheckSynchronize。这会将下一个方法排入队列并运行它,直到后一个方法完成后才返回。只有这样才会出现对话框。]

我试图确保从工作线程排队以在 VCL 线程中运行的所有方法都以严格的 FIFO 顺序运行,并且它们都在工作线程被销毁之前完成。

我的另一个限制是我试图保持 GUI 与业务逻辑的严格分离。在这种情况下,线程是业务逻辑层的一部分,因此我不能使用来自OnTerminate 处理程序的PostMessage 来安排销毁线程(正如其他地方的许多贡献者所建议的那样)。所以我在 TThread.Execute 退出之前在最终的排队方法中设置了FreeOnTerminate := True。 (因此需要它们以严格的 FIFO 顺序执行。)

这就是我的 TThread.Execute 方法的结束方式:

finally
  // Queue a final method to execute in the main thread that will set an event
  // allowing this thread to exit. This ensures that this thread can't exit
  // until all of the queued procedures have run.
  Queue(
    procedure
    begin
      if Assigned(fOnComplete) then
      begin
        fOnComplete(Self);
        // Handler sets fWorker.FreeOnTerminate := True and fWorker := nil
      end;
      SetEvent(fCanExit);
    end);
  WaitForSingleObject(fCanExit, INFINITE);
end;

但正如我所说,这不起作用,因为这个排队的方法在一些早期的排队方法之前执行。

任何人都可以提出一种简单而干净的方法来完成这项工作,或者一个简单而干净的替代方案吗?

[到目前为止,我想出的唯一一个保持关注点分离和模块化的想法是给我的TThread 子类一个自己的WndProc。然后我可以直接使用PostMessage 这个 WndProc 而不是主窗体。但我希望有更轻量级的东西。]


感谢到目前为止的答案和 cmets。我现在明白,我上面的带有排队的SetEventWaitForSingleObject 的代码在功能上等同于在最后调用Synchronize 而不是Queue,因为QueueSynchronize 共享同一个队列。我首先尝试了Synchronize,但它失败的原因与上面的代码失败的原因相同——早期的排队方法调用消息处理,因此最终的Synchronize 方法在早期的排队方法完成之前运行。

所以我仍然坚持原来的问题,现在归结为:我可以干净地确保所有排队的方法都在工作线程被释放之前完成,我是否可以在不使用 @ 的情况下干净地释放工作线程987654342@,需要一个窗口句柄才能发布到(我的业务层无权访问)。

我还更新了标题以更好地反映原始问题,尽管如果合适的话,我很乐意提供不使用TThread.Queue 的替代解决方案。如果有人能想出更好的标题,请编辑它。


另一个更新:David Heffernan 的This answer 建议在一般情况下使用PostMessage 和特殊的AllocateHWnd,如果TThread.Queue 不可用或不合适。值得注意的是,在主窗体中使用PostMessage 是绝对不安全的,因为窗口可以通过更改其句柄而自发地重新创建,这将导致旧句柄的所有后续消息丢失。这为我采用这个特定的解决方案提供了一个强有力的论据,因为在我的情况下创建隐藏窗口没有额外的开销,因为使用 PostMessageany 应用程序应该这样做 - 即我的关注点分离论点是无关紧要。

【问题讨论】:

  • 似乎队列在这里不是正确的选择。您可以考虑改用 Synchronize。
  • @UweRaabe 我应该说排队的方法需要与工作线程异步执行。工作线程只有在完成所有工作并准备退出时才能等待。
  • 恕我直言,all of the queued methods have completed before the worker thread is freed 表示此线程与在不同线程的上下文中执行的排队方法之间存在(太)强耦合
  • @mjn 出现这种情况是因为要求主线程可以随时终止工作线程。但是由于主线程不能使用通常的 PostMessage 技术将工作线程从其 OnTerminate 处理程序中释放出来,所以我们必须使用 FreeOnTerminate := True,但是直到所有排队的方法都完成后才能设置,因为所有方法仍在队列中当线程被销毁时,线程会被销毁。但我对实现目标的其他方式持开放态度。欢迎所有建议。
  • @mjm 实际上,如果线程有自己的 WndProc,我可以使用 PostMessage 来释放线程;可以说,无论如何,这是唯一可靠的方法。但是,如果我为此使用 Windows 消息传递,那么我还不如全力以赴并使用 Windows 消息传递而不是到处使用 TThread.Queue。说到这里,我还不如使用本机 Win32 API 而不是 TThread。但我正在寻找最简单的可行解决方案,不利用 VCL 似乎很愚蠢。

标签: multithreading delphi


【解决方案1】:

TThread.Queue() 是一个先进先出队列。事实上,它共享Thread.Sychronize() 使用的同一个队列。但是您是正确的,消息处理确实会导致排队的方法执行。这是因为每当消息队列在处理新消息后空闲时,TApplication.Idle() 就会调用 CheckSynchronize()。因此,如果队列/同步方法调用消息处理,即使之前的方法仍在运行,也可以允许其他队列/同步方法运行。

如果您想确保在线程终止之前调用队列方法,您应该使用Synchronize() 而不是Queue(),或者使用OnTerminate 事件(由Synchronize() 触发)。您在 finally 块中所做的实际上与 OnTerminate 事件在本机中所做的相同。

queued 方法中设置 FreeOnTerminate := True 要求内存泄漏。 FreeOnTerminateExecute() 退出时立即评估,在调用DoTerminate() 以触发OnTerminate 事件之前(我认为这是一个疏忽,因为早期评估它会阻止OnTerminate 在终止时决定是否OnTerminate 退出后线程是否应该释放自己)。因此,如果队列方法在Execute() 退出后运行,则无法保证FreeOnTerminate 会及时设置。在将控制权返回给线程之前等待 queued 方法完成正是Synchronize() 的目的。 Synchronize() 是同步的,它等待方法退出。 Queue() 是异步的,它根本不等待。

【讨论】:

  • 我阅读了您的其他答案,建议在 Execute 退出之前通过 Synchronize 调用 OnTerminate。当我尝试这个时,我发现 OnTerminate 似乎执行 before Queue()d 方法,所以我认为它跳过了队列。现在我知道这是因为排队的方法调用了消息处理,而我使用 WaitForSingleObject 编写的内容在功能上等同于 Synchronize。但是我仍然留下了最初的问题,即如何让线程释放自己,但只有在所有排队的方法都完成之后,排队的方法本身可能会调用消息处理。
【解决方案2】:

我通过在Execute() 方法的末尾添加对Synchronize() 的调用来解决此问题。这会强制线程等待所有使用Queue() 添加的调用在主线程上完成,然后才能调用使用Synchronize() 添加的调用。

TMyThread = class (TThread)
private
  procedure QueueMethod;
  procedure DummySync;
protected
  procedure Execute; override;
end;

procedure TMyThread.QueueMethod;
begin
  // Do something on the main thread 
  UpdateSomething;
end;

procedure TMyThread.DummySync;
begin
  // You don't need to do anything here. It's just used
  // as a fence to stop the thread ending before all the 
  // Queued messages are processed.
end;

procedure TMyThread.Execute;
begin
  while SomeCondition do 
  begin
     // Some process

     Queue(QueueMethod);
  end;
  Synchronize(DummySync);
end;

【讨论】:

  • 有史以来最简单的解决方案。
【解决方案3】:

这是我最终采用的解决方案。

我使用 Delphi TCountdownEvent 来跟踪我的线程中未完成的排队方法的数量,在排队方法之前增加计数,并在排队方法的最后一个动作中减少它。

就在我对TThread.Execute 的覆盖返回之前,它等待TCountdownEvent 对象发出信号,即当计数达到零时。这是保证所有排队的方法在Execute 返回之前完成的关键步骤。

一旦所有排队的方法都完成了,它就会使用 OnComplete 处理程序调用 Synchronize - 感谢 Remy 指出这相当于但比我使用 QueueWaitForSingleObject 的原始代码更简单. (OnComplete 类似于OnTerminate,但在 Execute 返回之前调用,以便处理程序可以修改FreeOnTerminate。)

唯一的问题是TCountdownEvent.AddCount 仅在计数已经大于零时才有效。于是我写了一个类助手来实现ForceAddCount

procedure TCountdownEventHelper.ForceAddCount(aCount: Integer);
begin
  if not TryAddCount(aCount) then
  begin
    Reset(aCount);
  end;
end;

通常这是有风险的,但在我的情况下,我们知道当线程开始等待队列中未完成的方法数量达到零时,不再有方法可以排队(因此,一旦计数达到零,它将保持为零)。

这并没有完全解决处理消息的排队方法的问题,因为个别排队的方法仍然可能出现乱序运行。但是我现在可以保证所有排队的方法都是异步运行的,但会在线程退出之前完成。这是主要目标,因为它允许线程自行清理而不会丢失排队的方法。

【讨论】:

    【解决方案4】:

    一些想法:

    1. 如果您希望线程自行删除,FreeOnTerminate 并不是世界末日。
    2. 信号量可让您在需要时保持计数,有这样的结构。
    3. 如果您想要一些细粒度的控制,没有什么可以阻止您编写或使用自己的队列原语和 AllocateHWnd。

    【讨论】:

      猜你喜欢
      • 2019-04-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-09-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多