应用程序在SysUtils中挂起 - >退出时DoneMonitorSupport

时间:2013-01-08 14:57:43

标签: multithreading delphi delphi-xe2

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

我已经进入系统单元并找到了程序进入无限循环的地方。它位于 SysUtils 行19868 - > DoneMonitorSupport - >的 CleanEventList

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

我在网上搜索了一个解决方案并找到了几个QC报告:

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

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

以前有人遇到过类似的行为吗?你知道有什么策略可以找到根本原因所在吗?

4 个答案:

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

  • SyncEventCacheArray - 这告诉德尔福而现在是否正在使用Lock以及
  • TSyncEventItem.Lock - 如果需要等待,它会保存用于同步的实际事件。

当应用程序终止时,TSyncEventItem.Event会遍历SysUtils.DoneMonitorSupport中的所有记录,并等待Lock变为ZERO,即等待锁定停止被任何内容使用。从理论上讲,只要该锁不是零,至少有一个线程可能正在使用锁 - 所以理所当然的事情就是等待,以免造成AccessViolations错误。我们终于得到了当前的问题:悬挂在SyncEventCacheArray

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

因为至少有一个使用SysUtils.DoneMonitorSupportNewSyncObject中的任何一个分配的事件未使用相应的NewWaitObjectFreeSyncObject而被释放。我们回到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;

换句话说,最终的同步事件只有在释放用于同步的对象时才会释放

回答OP的问题:

应用程序挂起,因为至少有一个用于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,它都会删除该事件。