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

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

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

我在网上搜索了一个解决方案,并找到了一些质量控制报告:
  • 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字段开始。当第一个线程在任何对象上使用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中的实现方式旨在为这些锁提供最少的锁和上下文切换;因此,对象本身(由NewSyncObjectNewWaitObject返回)不是直接由CreateEvent()返回的事件,而是指向SyncEventCacheArray中的记录的指针。更进一步,直到需要时才创建实际的Windows事件。因此,SyncEventCacheArray中的记录包含几个记录:
  • TSyncEventItem.Lock-这告诉Delphi而不是Lock正在被用于任何东西,以及
  • TSyncEventItem.Event-如果需要等待,它将保存将用于同步的实际事件。

  • 当应用程序终止时,SysUtils.DoneMonitorSupport会遍历SyncEventCacheArray中的所有记录,并等待Lock变为零,即等待锁停止被任何人使用。从理论上讲,只要该锁不为零,就可能有至少一个线程在使用该锁-因此,理智的事情是要等待,以免引起AccessViolations错误。最后,我们得出了当前的问题:卡在SysUtils.DoneMonitorSupport
    为什么即使所有线程都正确终止,应用程序也可能会卡在SysUtils.DoneMonitorSupport中?
    因为至少没有使用NewSyncObjectNewWaitObject中的任何一个分配的事件没有使用对应的FreeSyncObjectFreeWaitObject来释放。然后我们回到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/

    10-13 02:43