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都应该这样做 - 即我的关注点分离参数是无关紧要的.
TThread.Queue()是一个FIFO队列.实际上,它共享Thread.Sychronize()使用的队列.但是你说错了,消息处理会导致排队的方法执行.这是因为在处理新消息后消息队列空闲时TApplication.Idle()调用CheckSynchronize().因此,如果排队/同步方法调用消息处理,即使早期方法仍在运行,也可以允许其他排队/同步方法运行.
如果要确保在线程终止之前调用队列方法,则应该使用Synchronize()而不是Queue()使用或使用OnTerminate事件(由触发器Synchronize()).您在finally块中执行的操作实际上与OnTerminate事件本身已执行的操作相同.
设置FreeOnTerminate := True在一个排队的方法是要求一个内存泄漏. FreeOnTerminate在Execute()退出之前立即进行评估,之后DoTerminate()被调用以触发OnTerminate事件(在我看来这是一种疏忽,因为评估它早期阻止OnTerminate在终止时决定一个线程在OnTerminate退出后是否应该自行释放).因此,如果排队方法在Execute()退出后运行,则无法保证FreeOnTerminate将及时设置.在将控制权返回给线程之前等待排队的方法完全正是Synchronize()为了什么. Synchronize()是同步的,它等待方法退出. Queue()是异步的,它根本不会等待.
我通过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)
这是我最终采用的解决方案。
我使用 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)
通常这会有风险,但在我的情况下,我们知道当线程开始等待未完成的排队方法数量达到零时,不再有方法可以排队(因此从这一点开始,一旦计数达到零,它将保持为零)。
这并不能完全解决处理消息的排队方法的问题,因为各个排队方法仍然可能出现无序运行。但我现在可以保证所有排队方法都异步运行,但会在线程退出之前完成。这是主要目标,因为它允许线程自行清理,而不会有丢失排队方法的风险。
| 归档时间: |
|
| 查看次数: |
2590 次 |
| 最近记录: |