我正在编写一个非常占用线程的应用程序,该应用程序在退出时会挂起。
我已经探究了系统单元,并找到了程序进入无限循环的地方。在 SysUtils 第19868行中-> DoneMonitorSupport -> CleanEventList :
repeat until InterlockedCompareExchange(EventCache[I].Lock, 1, 0) = 0;
我在网上搜索了一个解决方案,并找到了一些质量控制报告:
不幸的是,这些似乎与我的情况无关,因为我既不使用 TThreadList 也不使用 TMonitor 。
我非常确定我的所有线程都已完成并被销毁,因为所有线程都从保持创建/销毁计数的基本线程继承。
有人遇到过类似的行为吗?您是否知道用于发现根本原因的任何策略?
最佳答案
我一直在研究TMonitor
锁的实现方式,最后我做了一个有趣的发现。关于戏剧性,我首先会告诉您锁的工作方式。
当您在TMonitor
上调用任何TObject
函数时,将创建TMonitor
记录的新实例,并将该实例分配给对象本身内部的MonitorFld
。使用InterlockedCompareExchangePointer
以线程安全的方式进行此分配。由于这个技巧,TObject
仅包含一个指针大小的数据量来支持TMonitor
,它不包含完整的TMonitor结构。那是一件好事。
此TMonitor
结构包含许多记录。我们将从FLockCount: Integer
字段开始。当第一个线程在任何对象上使用TMonitor.Enter()
时,此组合的锁定计数器字段的值为零。再次使用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
指针将实际分配Event的职责委托(delegate)给另一个单元。指向TMonitorSupport
类型的记录,该记录声明了5个函数指针:NewSyncObject
-为同步目的分配新事件FreeSyncObject
-取消分配为同步目的分配的事件NewWaitObject
-为等待操作分配新的事件FreeWaitObject
-释放该等待事件WaitAndOrSignalObject
-好..等待或发出信号。 事实还证明,
NewXYZ
函数返回的对象可以是任何对象,因为它们仅用于对WaitXYZ
的调用以及对FreeXyzObject
的相应调用。这些函数在SysUtils
中的实现方式旨在为这些锁提供最少的锁和上下文切换;因此,对象本身(由NewSyncObject
和NewWaitObject
返回)不是直接由CreateEvent()
返回的事件,而是指向SyncEventCacheArray
中的记录的指针。更进一步,直到需要时才创建实际的Windows事件。因此,SyncEventCacheArray
中的记录包含几个记录:TSyncEventItem.Lock
-这告诉Delphi而不是Lock正在被用于任何东西,以及TSyncEventItem.Event
-如果需要等待,它将保存将用于同步的实际事件。 当应用程序终止时,
SysUtils.DoneMonitorSupport
会遍历SyncEventCacheArray
中的所有记录,并等待Lock变为零,即等待锁停止被任何人使用。从理论上讲,只要该锁不为零,就可能有至少一个线程在使用该锁-因此,理智的事情是要等待,以免引起AccessViolations错误。最后,我们得出了当前的问题:卡在SysUtils.DoneMonitorSupport
中为什么即使所有线程都正确终止,应用程序也可能会卡在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
。换句话说,仅当释放用于同步的对象时,才释放最终的同步事件!
回答OP的问题:
该应用程序挂起,因为至少一个用于
TMonitor.Enter()
的对象未释放。可能的解决方案:
不幸的是我不喜欢这样。这是不对的,我的意思是不释放小对象的惩罚应该是小内存泄漏,而不是挂起的应用程序!对于服务应用程序而言,这尤其有害,在服务应用程序中,服务可能只是永远挂起,无法完全关闭,但无法响应任何请求。
德尔福团队的解决方案?它们不应该卡在
SysUtils
单元的完成代码中,不要紧。他们可能应该忽略Lock
并转到关闭事件句柄。在那个阶段(SysUtils单元的定稿),如果仍有代码在某个线程中运行,则由于大多数单元已定稿,因此它的状态确实很差,它不在设计用于运行的环境中运行。对于delphi用户?我们可以用自己的版本替换
MonitorSupport
,该版本在定稿时不会进行那些广泛的测试。关于multithreading - 应用程序在退出时卡在SysUtils-> DoneMonitorSupport中,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/14217735/