我一直在研究TMonitor
锁是如何实现的,终于有了一个有趣的发现。为了有点戏剧性,我先告诉你锁是如何工作的。
当您在 a 上调用任何TMonitor
函数时TObject
,会创建一个新的TMonitor
记录实例,并将该实例分配给MonitorFld
对象本身内部的 a。此分配以线程安全的方式进行,使用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
指针将实际分配事件的职责委托给了另一个单元。这指向TMonitorSupport
声明 5 个函数指针的类型记录:
NewSyncObject
- 为同步目的分配一个新事件
FreeSyncObject
- 取消分配用于同步目的的事件
NewWaitObject
- 为等待操作分配一个新事件
FreeWaitObject
- 释放等待事件
WaitAndOrSignalObject
- 好吧..等待或发出信号。
事实证明,NewXYZ
函数返回的对象可以是任何东西,因为它们仅用于调用WaitXYZ
和对应调用FreeXyzObject
. 这些功能的实现SysUtils
方式旨在为这些锁提供最少的锁定和上下文切换;因为对象本身(由NewSyncObject
and返回NewWaitObject
)不是直接由 . 返回的事件CreateEvent()
,而是指向SyncEventCacheArray
. 它更进一步,直到需要时才创建实际的 Windows 事件。因此, 中的记录SyncEventCacheArray
包含几条记录:
TSyncEventItem.Lock
- 这告诉 Delphi 而不是 Lock 现在是否正在用于任何事情,并且
TSyncEventItem.Event
- 如果需要等待,这包含将用于同步的实际事件。
当应用程序终止时,SysUtils.DoneMonitorSupport
遍历所有记录SyncEventCacheArray
并等待锁变为零,即等待锁停止被任何东西使用。从理论上讲,只要该锁不为零,至少有一个线程可能正在使用该锁 - 所以明智的做法是等待,以免导致 AccessViolations 错误。我们终于解决了当前的问题:HANGING inSysUtils.DoneMonitorSupport
为什么应用程序可能会在 SysUtils.DoneMonitorSupport 中挂起,即使它的所有线程都正确终止?
因为至少一个使用 或 中的任何一个分配的事件NewSyncObject
没有使用它对应的orNewWaitObject
释放。我们回到常规。它分配的事件保存在对应于用于的对象的记录中。指向该记录的指针仅保存在该对象的实例数据中,并在应用程序的整个生命周期内保存在那里。搜索字段的名称,我们在文件中找到: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的问题:
应用程序挂起,因为至少有一个用于的 OBJECTTMonitor.Enter()
未被释放。
可能的解决方案:
不幸的是我不喜欢这个。不对,我的意思是不释放小对象的惩罚应该是小内存泄漏,而不是挂起的应用程序!这对于服务应用程序来说尤其糟糕,因为服务可能只是永远挂起,没有完全关闭但无法响应任何请求。
Delphi 团队的解决方案?他们不应该挂在SysUtils
单元的最终代码中,无论如何。他们可能应该忽略Lock
并转而关闭事件句柄。在那个阶段(SysUtils 单元的最终确定),如果仍然有代码在某个线程中运行,那么它的状态就非常糟糕,因为大多数单元都已经完成了,它没有在设计运行的环境中运行。
对于德尔福用户?我们可以MonitorSupport
用我们自己的版本替换它,它不会在最终确定时进行那些广泛的测试。