nor*_*aul 16 delphi multithreading delphi-xe2
我正在编写一个非常线程密集的应用程序,它会在退出时挂起.
我已经跟踪到系统单元并找到了程序进入无限循环的地方.它在sysutils的线19868 - > DoneMonitorSupport - > CleanEventList:
repeat until InterlockedCompareExchange(EventCache[I].Lock, 1, 0) = 0;
我在网上搜索了一个解决方案,发现了几个QC报告:
不幸的是,这些似乎与我的情况无关,因为我不使用TThreadList或TMonitor.
我很确定我的所有线程都已完成并且已被销毁,因为所有线程都继承自保持创建/销毁计数的基本线程.
以前有人遇到过类似的行为吗?你知道任何发现根本原因的策略吗?
Cos*_*und 15
我一直在研究TMonitor锁的实现方式,最后我发现了一个有趣的发现.对于一些戏剧,我首先会告诉你锁是如何工作的.
当您调用TMonitor某个函数时TObject,TMonitor会创建一个新的记录实例,并将该实例分配给MonitorFld该对象本身.这个赋值是以线程安全的方式使用InterlockedCompareExchangePointer.由于这个技巧TObject只包含一个指针大小的数据量供支持TMonitor,它不包含完整的TMonitor结构.这是一件好事.
该TMonitor结构包含许多记录.我们将从该FLockCount: Integer领域开始.当第一个线程用于TMonitor.Enter()任何对象时,此组合锁定计数器字段将具有值ZERO.再次使用InterlockedCompareExchange方法获取锁并启动计数器.调用线程没有锁定,没有上下文切换,因为这都是在进程中完成的.
当第二个线程尝试TMonitor.Enter()同一个对象时,它首次尝试锁定将失败.当发生这种情况时,Delphi遵循两种策略:
TMonitor.SetSpinCount()设置一些"旋转",那么Delphi将执行忙等待循环,旋转给定次数.这对于小锁是非常好的,因为它允许在不进行上下文切换的情况下获取锁.TMonitor.Enter()将在返回的事件上启动等待TMonitor.GetEvent().换句话说,它不会忙 - 等待浪费CPU周期.记住TMonitor.GetEvent()因为这非常重要.假设我们有一个获取锁的线程和一个试图获取锁的线程,但现在正在等待返回的事件TMonitor.GetEvent.当第一个线程调用时,TMonitor.Exit()它会注意到(通过FLockCount字段)至少有一个其他线程阻塞.因此它立即发出通常应该是先前分配的事件(调用TMonitor.GetEvent())的脉冲.但是由于两个线程,即调用的线程TMonitor.Exit()和调用的线程TMonitor.Enter()可能实际上同时调用TMonitor.GetEvent(),所以内部还有一些技巧TMonitor.GetEvent()可以确保只分配一个事件,与操作顺序无关.
对于一些更有趣的时刻,我们现在将深入研究TMonitor.GetEvent()工作方式.这个东西存在于System单元内部(你知道,我们无法重新编译的那个),但事实证明它通过System.MonitorSupport指针将实际分配事件的任务委托给另一个单元.这指向TMonitorSupport声明5个函数指针的类型记录:
NewSyncObject - 为同步目的分配新事件FreeSyncObject - 释放为同步目的分配的事件NewWaitObject - 为等待操作分配新事件FreeWaitObject - 释放Wait事件WaitAndOrSignalObject - 好......等待或发出信号.事实证明,NewXYZ函数返回的对象可以是任何东西,因为它们仅用于对WaitXYZ相应调用的调用FreeXyzObject.实现这些功能的方式SysUtils旨在为这些锁提供最少量的锁定和上下文切换; 因为对象本身(由NewSyncObject和返回NewWaitObject)不是直接返回的事件CreateEvent(),而是指向记录中的指针SyncEventCacheArray.更进一步,直到需要时才会创建实际的Windows事件.因为它中的记录SyncEventCacheArray包含几个记录:
TSyncEventItem.Lock - 这告诉Delphi,而Lock现在正在被用于任何事情TSyncEventItem.Event - 如果需要等待,这将保存将用于同步的实际事件.当应用程序终止时,SysUtils.DoneMonitorSupport将遍历所有记录SyncEventCacheArray并等待Lock变为ZERO,即等待锁定停止被任何东西使用.从理论上讲,只要该锁不是零,至少有一个线程可能正在使用锁 - 所以理所当然的事情就是等待,以免造成AccessViolations错误.我们终于得到了当前的问题:悬挂在SysUtils.DoneMonitorSupport
因为至少有一个事件使用任何一个事件分配NewSyncObject或者NewWaitObject没有使用它的相应FreeSyncObject或者被释放FreeWaitObject.我们回到TMonitor.GetEvent()例程.它分配的事件保存在TMonitor与用于的对象相对应的记录中TMonitor.Enter().指向该记录的指针仅保存在该对象的实例数据中,并在应用程序的生命周期内保留.搜索字段的名称FLockEvent,我们在System.pas文件中找到:
procedure TMonitor.Destroy;
begin
  if (MonitorSupport <> nil) and (FLockEvent <> nil) then
    MonitorSupport.FreeSyncObject(FLockEvent);
  Dispose(@Self);
end;
并在这里调用那个记录析构函数:procedure TObject.CleanupInstance.
换句话说,只有在释放用于同步的对象时才会释放最终的sync-event !
应用程序挂起,因为至少有一个用于的OBJECT TMonitor.Enter()未被释放.
不幸的是我不喜欢这个.这是不对的,我的意思是不释放一个小对象的惩罚应该是一个小的内存泄漏,而不是一个悬挂的应用程序!这对于服务应用程序来说尤其糟糕,因为服务应用程序可能只是永久挂起,而不是完全关闭但无法响应任何请求.
Delphi团队的解决方案?它们不应该挂在SysUtils单元的最终代码中,无论如何.他们应该忽略Lock并转向关闭Event句柄.在那个阶段(SysUtils单元的最终确定),如果某些线程中仍然存在代码运行,那么它的形状会非常糟糕,因为大多数单元已经完成,它不会在设计运行的环境中运行.
对于delphi用户?我们可以MonitorSupport用我们自己的版本替换它,在最终确定时不进行那些广泛的测试.