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

时间:2014-04-29 08:35:00

标签: multithreading delphi

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

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

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

我的另一个限制是我试图保持GUI与业务逻辑的严格分离。在这种情况下,线程是业务逻辑层的一部分,因此我不能使用PostMessage处理程序中的OnTerminate来安排线程被销毁(正如其他许多贡献者所推荐的那样)。所以我在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而不是主窗体。但是我希望能有一些更轻量级的东西。]


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

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

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


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

4 个答案:

答案 0 :(得分:6)

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

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

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

答案 1 :(得分:2)

我通过在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;

答案 2 :(得分:1)

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

我使用Delphi TCountdownEvent来跟踪我的线程中未完成的排队方法的数量,在排队方法之前递增计数,并将其作为排队方法的最终行为递减。

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

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

唯一的问题是OnTerminate只有在计数已经大于零时才有效。所以我编写了一个类助手来实现FreeOnTerminate

TCountdownEvent.AddCount

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

这并没有完全解决处理邮件的排队方法的问题,因为单个排队的方法仍然可能看起来无序运行。但我现在确保所有排队的方法异步运行但在线程退出之前就已完成。这是主要目标,因为它允许线程自我清理而不会丢失排队方法的风险。

答案 3 :(得分:-1)

一些想法:

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