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

标签 multithreading delphi delphi-xe2

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

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

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

我在网上搜索了一个解决方案,发现了几个 QC 报告:
  • http://qc.embarcadero.com/wc/qcmain.aspx?d=95194
  • http://qc.embarcadero.com/wc/qcmain.aspx?d=90487

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

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

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

    最佳答案

    我一直在看TMonitor实现了锁,终于有了一个有趣的发现。为了有点戏剧性,我将首先告诉你锁是如何工作的。
    当您拨打任何电话时 TMonitor TObject 上的函数, TMonitor 的新实例创建记录并将该实例分配给 MonitorFld在对象本身内部。这个赋值是以线程安全的方式进行的,使用 InterlockedCompareExchangePointer .因为这个伎俩TObject仅包含一个指针大小的数据量以支持 TMonitor ,它不包含完整的 TMonitor 结构。这是一件好事。
    TMonitor结构包含许多记录。我们将从 FLockCount: Integer 开始 field 。当第一个线程使用 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 将实际分配事件的职责委托(delegate)给另一个单元。指针。这指向了 TMonitorSupport 类型的记录声明了 5 个函数指针:
  • NewSyncObject - 为同步目的分配一个新事件
  • FreeSyncObject - 释放为同步目的分配的事件
  • NewWaitObject - 为等待操作分配一个新事件
  • FreeWaitObject - 解除等待事件
  • WaitAndOrSignalObject - 嗯.. 等待或信号。

  • 事实证明,NewXYZ 返回的对象函数可以是任何东西,因为它们只用于调用 WaitXYZ以及对 FreeXyzObject 的相应调用.这些函数在 SysUtils 中的实现方式旨在为这些锁提供最少量的锁定和上下文切换;因为对象本身(由 NewSyncObjectNewWaitObject 返回)不是直接由 CreateEvent() 返回的事件, 但指向 SyncEventCacheArray 中的记录的指针.更进一步,直到需要时才会创建实际的 Windows 事件。因此,SyncEventCacheArray 中的记录包含几个记录:
  • TSyncEventItem.Lock - 这告诉 Delphi 是否现在正在将 Lock 用于任何事情和
  • TSyncEventItem.Event - 如果需要等待,这将保存将用于同步的实际事件。

  • 当应用程序终止时,SysUtils.DoneMonitorSupport遍历 SyncEventCacheArray 中的所有记录并等待锁变为零,即等待锁停止被任何东西使用。从理论上讲,只要该锁不为零,则至少有一个线程可能正在使用该锁 - 因此明智的做法是等待,以免导致 AccessViolations 错误。我们终于解决了当前的问题:HANGING in SysUtils.DoneMonitorSupport为什么应用程序可能会在 SysUtils.DoneMonitorSupport 中挂起,即使它的所有线程都正确终止了?
    因为使用 NewSyncObject 中的任何一个分配了至少一个 Event或 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() 的 OBJECT没有被释放。
    可能的解决方案:
    不幸的是我不喜欢这个。这是不对的,我的意思是不释放一个小对象的惩罚应该是一个小的内存泄漏,而不是一个挂起的应用程序!这对于服务应用程序尤其不利,因为服务可能会永远挂起,不会完全关闭但无法响应任何请求。
    Delphi 团队的解决方案?他们不应该卡在 SysUtils 的终结代码中。单位,无论如何。他们可能应该忽略 Lock并移至关闭事件句柄。在那个阶段(SysUtils 单元的最终确定),如果仍然有代码在某个线程中运行,那么它的状态非常糟糕,因为大多数单元已经完成,它没有在其设计运行的环境中运行。
    对于delphi用户?我们可以更换 MonitorSupport使用我们自己的版本,在最终确定时不进行那些广泛测试的版本。

    关于multithreading - 退出时应用程序在 SysUtils -> DoneMonitorSupport 中挂起,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/14217735/

    相关文章:

    delphi - 消除 TSpeedButton 的闪烁

    delphi - 参数化类型的方法在制作通用接口(interface)工厂时不能使用局部符号错误

    java - 加入线程时处理异常的最佳方法

    c++ - 在 C++ 中执行并行任务而不等待结果

    windows - 如何在我自己的窗口下捕获屏幕,不包括我自己的窗口

    Delphi XE2后台IDE编译器找不到源路径

    Delphi XE2 新服务 - 为什么它包含这些 VCL 单元?

    java - 移动 ImageView 对象时的并发处理

    python 线程 - 将参数传递给线程的最佳方法

    delphi - 如果某些编译器选项(如优化)打开,如何在运行时检测?