16

我正在编写一个线程密集型应用程序,它在退出时挂起。

我跟踪到系统单元,找到了程序进入无限循环的地方。它在SysUtils行 19868 -> DoneMonitorSupport -> CleanEventList

repeat until InterlockedCompareExchange(EventCache[I].Lock, 1, 0) = 0;

我在网上搜索了一个解决方案,发现了几个 QC 报告:

不幸的是,这些似乎与我的情况无关,因为我既不使用TThreadList也不使用TMonitor

我很确定我的所有线程都已完成并已被销毁,因为它们都继承自保持创建/销毁计数的基本线程。

有没有人遇到过类似的行为?您是否知道发现根本原因所在的任何策略?

4

4 回答 4

15

我一直在研究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方式旨在为这些锁提供最少的锁定和上下文切换;因为对象本身(由NewSyncObjectand返回NewWaitObject)不是直接由 . 返回的事件CreateEvent(),而是指向SyncEventCacheArray. 它更进一步,直到需要时才创建实际的 Windows 事件。因此, 中的记录SyncEventCacheArray包含几条记录:

  • TSyncEventItem.Lock- 这告诉 Delphi 而不是 Lock 现在是否正在用于任何事情,并且
  • TSyncEventItem.Event- 如果需要等待,这包含将用于同步的实际事件。

当应用程序终止时,SysUtils.DoneMonitorSupport遍历所有记录SyncEventCacheArray并等待锁变为零,即等待锁停止被任何东西使用。从理论上讲,只要该锁不为零,至少有一个线程可能正在使用该锁 - 所以明智的做法是等待,以免导致 AccessViolations 错误。我们终于解决了当前的问题:HANGING inSysUtils.DoneMonitorSupport

为什么应用程序可能会在 SysUtils.DoneMonitorSupport 中挂起,即使它的所有线程都正确终止?

因为至少一个使用 或 中的任何一个分配的事件NewSyncObject没有使用它对应的orNewWaitObject释放。我们回到常规。它分配的事件保存在对应于用于的对象的记录中。指向该记录的指针保存在该对象的实例数据中,并在应用程序的整个生命周期内保存在那里。搜索字段的名称,我们在文件中找到:FreeSyncObjectFreeWaitObjectTMonitor.GetEvent()TMonitorTMonitor.Enter()FLockEventSystem.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用我们自己的版本替换它,它不会在最终确定时进行那些广泛的测试。

于 2013-01-09T00:39:31.153 回答
1

我可以使用 Cosmin 提供的示例重现您的问题。我也可以通过在所有线程完成后简单地释放 SyncObj 来解决问题。

由于我无法访问您的代码,所以我不能多说,但可能 TMonitor 使用的某些对象实例没有被释放。

于 2013-01-09T00:18:15.047 回答
1

我通过以下方式解决了这个错误:

System.SysUtilsInterlockedAPIs.incEncodingData.inc复制到我的应用程序目录并更改System.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}

在这些更改之后,我的应用程序正确关闭。

注意:我尝试按照 LU RD 的建议添加“ReportMemoryLeaksOnShutdown”,但在关闭时,我的应用程序进入了一个竞争条件,弹出了许多运行时错误对话框。当我尝试 EurekaLog 的内存泄漏功能时,也会发生类似的事情。

于 2013-01-09T10:43:10.663 回答
1

在 Delphi XE5 中,Embarcadero 通过添加(Now - Start > 1 / MSecsPerDay) orrepeat until循环中解决了这个问题,CleanEventList这样它会在 1 毫秒后放弃。然后它会删除该事件,无论是否Lock0

于 2016-02-10T01:37:54.130 回答