确保在线程自毁之前完成所有TThread.Queue方法

Ian*_*dby 8 delphi multithreading

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

[编辑:实际上他们确实以FIFO顺序开始.有了ShowMessage它看起来像一个后来跑了第一,因为有一个呼叫CheckSynchronize出现的对话框中之前.这会使下一个方法出错并运行它,直到后一个方法完成才返回.只有这样才会出现对话框.]

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

我的另一个限制是我试图保持GUI与业务逻辑的严格分离.在这种情况下,线程是业务逻辑层的一部分,所以我不能使用PostMessage从一个OnTerminate处理程序的线程被破坏(如在别处推荐由多个贡献者)安排.所以我FreeOnTerminate := True在TThread.Execute退出之前设置了最终的排队方法.(因此需要它们以严格的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;
Run Code Online (Sandbox Code Playgroud)

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

任何人都可以建议一个简单而干净的方法来完成这项工作,或者一个简单而干净的选择吗?

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


感谢目前为止的答案和评论.我现在明白我上面的代码排队了,SetEvent并且WaitForSingleObject在功能上等同Synchronize于在末尾调用而不是Queue因为Queue并且Synchronize共享相同的队列.我Synchronize首先尝试并且失败的原因与上面的代码失败的原因相同 - 早期排队的方法调用消息处理,因此最终Synchronize方法在早期排队方法完成之前运行.

所以我仍然坚持原来的问题,现在归结为:我可以干净地确保在释放工作线程之前所有排队的方法都已完成,并且我可以干净地释放工作线程而不使用PostMessage,这需要一个要发布到的窗口句柄(我的业务层无法访问).

我还更好地更新了标题以反映最初的问题,尽管我很乐意寻找一种TThread.Queue在适当情况下不使用的替代解决方案.如果有人能想出更好的标题,请编辑它.


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

Rem*_*eau 6

TThread.Queue()是一个FIFO队列.实际上,它共享Thread.Sychronize()使用的队列.但是你说错了,消息处理会导致排队的方法执行.这是因为在处理新消息后消息队列空闲时TApplication.Idle()调用CheckSynchronize().因此,如果排队/同步方法调用消息处理,即使早期方法仍在运行,也可以允许其他排队/同步方法运行.

如果要确保在线程终止之前调用队列方法,则应该使用Synchronize()而不是Queue()使用或使用OnTerminate事件(由触发器Synchronize()).您在finally块中执行的操作实际上与OnTerminate事件本身已执行的操作相同.

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


Nat*_*Nat 5

我通过Synchronize()在我的Execute()方法末尾添加一个调用来解决这个问题。这会强制线程等待所有添加的调用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;
Run Code Online (Sandbox Code Playgroud)


Ian*_*dby 1

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

我使用 DelphiTCountdownEvent来跟踪线程中未完成的排队方法的数量,在对方法进行排队之前增加计数,并在排队方法的最后操作时减少计数。

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

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

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

procedure TCountdownEventHelper.ForceAddCount(aCount: Integer);
begin
  if not TryAddCount(aCount) then
  begin
    Reset(aCount);
  end;
end;
Run Code Online (Sandbox Code Playgroud)

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

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