我正在编写一个线程非常密集的应用程序,它会在退出时挂起。
我已经进入系统单元并找到了程序进入无限循环的地方。它位于 SysUtils 行19868 - > DoneMonitorSupport - >的 CleanEventList :
repeat until InterlockedCompareExchange(EventCache[I].Lock, 1, 0) = 0;
我在网上搜索了一个解决方案并找到了几个QC报告:
不幸的是,这些似乎与我的情况无关,因为我不使用 TThreadList 或 TMonitor 。
我很确定我的所有线程都已完成并且已被销毁,因为所有线程都继承自保持创建/销毁计数的基本线程。
以前有人遇到过类似的行为吗?你知道有什么策略可以找到根本原因所在吗?
答案 0 :(得分:14)
我一直在研究如何实施TMonitor
锁定,最后我发现了一个有趣的发现。对于一些戏剧,我首先会告诉你锁是如何工作的。
当您在TMonitor
上调用任何TObject
函数时,会创建TMonitor
记录的新实例,并将该实例分配给对象本身内的MonitorFld
。使用InterlockedCompareExchangePointer
以线程安全的方式进行此分配。由于这个技巧,TObject
只包含一个指针大小的数据量,用于TMonitor
的支持,它不包含完整的TMonitor结构。这是一件好事。
此TMonitor
结构包含许多记录。我们将从FLockCount: Integer
字段开始。当第一个线程在任何对象上使用TMonitor.Enter()
时,此组合锁定计数器字段将具有值ZERO。再次使用InterlockedCompareExchange
方法获取锁定并启动计数器。调用线程没有锁定,没有上下文切换,因为这都是在进程中完成的。
当第二个线程尝试TMonitor.Enter()
同一个对象时,它首次尝试锁定将失败。当发生这种情况时,德尔福遵循两种策略:
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
单元内(你知道,我们可以不重新编译),但事实证明它通过{将实际分配事件的职责委托给另一个单元。 {1}}指针。这表示类型System.MonitorSupport
的记录声明了5个函数指针:
TMonitorSupport
- 为同步目的分配新事件NewSyncObject
- 释放为同步目的分配的事件FreeSyncObject
- 为等待操作分配新事件NewWaitObject
- 释放Wait事件FreeWaitObject
- 嗯..等待或发出信号。事实证明,WaitAndOrSignalObject
函数返回的对象可以是任何内容,因为它们仅用于调用NewXYZ
以及对WaitXYZ
的相应调用。 }。这些函数在FreeXyzObject
中实现的方式旨在为这些锁提供最少量的锁定和上下文切换;因此,对象本身(由SysUtils
和NewSyncObject
返回)不是直接由NewWaitObject
返回的事件,而是指向CreateEvent()
中记录的指针。更进一步,直到需要时才会创建实际的Windows事件。因此,SyncEventCacheArray
中的记录包含几条记录:
SyncEventCacheArray
- 这告诉德尔福而现在是否正在使用Lock以及TSyncEventItem.Lock
- 如果需要等待,它会保存用于同步的实际事件。当应用程序终止时,TSyncEventItem.Event
会遍历SysUtils.DoneMonitorSupport
中的所有记录,并等待Lock变为ZERO,即等待锁定停止被任何内容使用。从理论上讲,只要该锁不是零,至少有一个线程可能正在使用锁 - 所以理所当然的事情就是等待,以免造成AccessViolations错误。我们终于得到了当前的问题:悬挂在SyncEventCacheArray
因为至少有一个使用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
的OBJECT未被释放。
不幸的是,我不喜欢这样。它不对,我的意思是不释放一个小物体的惩罚应该是一个小的内存泄漏,而不是一个悬挂的应用程序!这对服务应用程序来说尤其糟糕,因为服务应用程序可能只是永久挂起,而不是完全关闭但无法响应任何请求。
Delphi团队的解决方案?它们不应该挂在TMonitor.Enter()
单元的终结代码中,无论如何。他们应该忽略SysUtils
并转向关闭事件句柄。在那个阶段(SysUtils单元的最终确定),如果在某个线程中仍有代码运行,那么由于大多数单元已经完成,它的形状真的很糟糕,所以它没有运行设计运行的环境。
对于delphi用户?我们可以用我们自己的版本替换Lock
,这个版本在最终确定时没有进行那些广泛的测试。
答案 1 :(得分:1)
我可以使用Cosmin提供的示例重现您的问题。我也可以通过在所有线程完成后释放SyncObj来解决问题。
由于我无法访问您的代码,我不能多说,但可能没有释放TMonitor使用的某个对象实例。
答案 2 :(得分:1)
我通过以下方式解决了这个问题:
将 System.SysUtils , InterlockedAPIs.inc 和 EncodingData.inc 复制到我的应用程序目录,并在系统中更改以下代码.SysUtils 强>:
procedure CleanEventList(var EventCache: array of TSyncEventItem);
var
I: Integer;
begin
for I := Low(EventCache) to High(EventCache) do
begin
if InterlockedCompareExchange(EventCache[I].Lock, 1, 0) = 0 then
DeleteSyncWaitObj(EventCache[I].Event);
//repeat until InterlockedCompareExchange(EventCache[I].Lock, 1, 0) = 0;
//DeleteSyncWaitObj(EventCache[I].Event);
end;
end;
我还在System.SysUtils的顶部添加了这个检查,以提醒我在更改Delphi版本时更新System.SysUtils文件:
{$IFNDEF VER230}
!!!!!!!!!!!!!!!!
You need to update this unit to fix the bug at line 19868
See http://stackoverflow.com/questions/14217735/application-hangs-in-sysutils-donemonitorsupport-on-exit
!!!!!!!!!!!!!!!!
{$ENDIF}
完成这些更改后,我的应用程序正确关闭。
注意:我尝试添加“ReportMemoryLeaksOnShutdown”作为LU RD建议,但在关机时我的应用程序进入竞争状态,弹出许多运行时错误对话框。当我尝试EurekaLog的内存泄漏功能时,会发生类似的事情。
答案 3 :(得分:1)
在Delphi XE5中,Embarcadero通过将(Now - Start > 1 / MSecsPerDay) or
添加到repeat until
中的CleanEventList
循环来解决这个问题,以便它在1毫秒后放弃。然后,无论Lock
是否为0
,它都会删除该事件。