线程关闭期间Win64 Delphi RTL中的内存泄漏?

Posted

技术标签:

【中文标题】线程关闭期间Win64 Delphi RTL中的内存泄漏?【英文标题】:Memory leak in the Win64 Delphi RTL during thread shutdown? 【发布时间】:2012-05-19 21:42:17 【问题描述】:

很长一段时间以来,我都注意到我的服务器应用程序的 Win64 版本会泄漏内存。虽然 Win32 版本在内存占用相对稳定的情况下运行良好,但 64 位版本使用的内存定期增加 - 可能 20Mb/天,没有任何明显的原因(不用说,FastMM4 没有报告任何内存泄漏) . 32 位和 64 位版本的源代码相同。该应用程序是围绕 Indy TIdTCPServer 组件构建的,它是一个高度多线程的服务器,连接到数据库,处理由 Delphi XE2 生成的其他客户端发送的命令。

我花了很多时间检查自己的代码并试图理解为什么 64 位版本会泄漏这么多内存。我最终使用了旨在跟踪内存泄漏的 MS 工具,如 DebugDiag 和 XPerf,似乎 Delphi 64 位 RTL 中存在一个基本缺陷,每次线程与 DLL 分离时都会导致一些字节泄漏。对于必须 24/7 不间断运行且无需重新启动的高度多线程应用程序,此问题尤其严重。

我用一个非常基本的项目重现了这个问题,该项目由一个主机应用程序和一个库组成,两者都是用 XE2 构建的。 DLL 与主机应用程序静态链接。宿主应用程序创建的线程只调用虚拟导出过程并退出:

这里是库的源代码:

library FooBarDLL;

uses
  Windows,
  System.SysUtils,
  System.Classes;

$R *.res

function FooBarProc(): Boolean; stdcall;
begin
  Result := True; //Do nothing.
end;

exports
  FooBarProc;

宿主应用程序使用一个计时器来创建一个只调用导出过程的线程:

  TFooThread = class (TThread)
  protected
    procedure Execute; override;
  public
    constructor Create;
  end;

...

function FooBarProc(): Boolean; stdcall; external 'FooBarDll.dll';

implementation

$R *.dfm

procedure THostAppForm.TimerTimer(Sender: TObject);
begin
  with TFooThread.Create() do
    Start;
end;

 TFooThread 

constructor TFooThread.Create;
begin
  inherited Create(True);
  FreeOnTerminate := True;
end;

procedure TFooThread.Execute;
begin
  /// Call the exported procedure.
  FooBarProc();
end;

这里是一些使用 VMMap 显示泄漏的屏幕截图(查看名为“Heap”的红线)。以下屏幕截图是在 30 分钟内拍摄的。

32位二进制显示增加了16字节,完全可以接受:

64位二进制显示增加了12476字节(从820K到13296K),问题比较多:

堆内存的不断增加也得到了XPerf的证实:

XPerf usage

使用 DebugDiag 我能够看到分配泄漏内存的代码路径:

LeakTrack+13529
<my dll>!Sysinit::AllocTlsBuffer+13
<my dll>!Sysinit::InitThreadTLS+2b
<my dll>!Sysinit::::GetTls+22
<my dll>!System::AllocateRaiseFrame+e
<my dll>!System::DelphiExceptionHandler+342
ntdll!RtlpExecuteHandlerForException+d
ntdll!RtlDispatchException+45a
ntdll!KiUserExceptionDispatch+2e
KERNELBASE!RaiseException+39
<my dll>!System::::RaiseAtExcept+106
<my dll>!System::::RaiseExcept+1c
<my dll>!System::ExitDll+3e
<my dll>!System::::Halt0+54
<my dll>!System::::StartLib+123
<my dll>!Sysinit::::InitLib+92
<my dll>!Smart::initialization+38
ntdll!LdrShutdownThread+155
ntdll!RtlExitUserThread+38
<my application>!System::EndThread+20
<my application>!System::Classes::ThreadProc+9a
<my application>!SystemThreadWrapper+36
kernel32!BaseThreadInitThunk+d
ntdll!RtlUserThreadStart+1d

Remy Lebeau helped me on the Embarcadero forums 了解发生了什么:

第二次泄漏看起来更像是一个明确的错误。线程期间 关机,StartLib() 被调用,它调用 ExitThreadTLS() 来 释放调用线程的 TLS 内存块,然后调用 Halt0() 调用 ExitDll() 来引发一个被 DelphiExceptionHandler() 调用 AllocateRaiseFrame(),它 间接调用 GetTls() 并因此调用 InitThreadTLS() 名为 ExceptionObjectCount 的 threadvar 变量。重新分配 仍在进程中的调用线程的 TLS 内存块 被关闭。所以要么 StartLib() 不应该调用 在 DLL_THREAD_DETACH 期间 Halt0(),否则 DelphiExceptionHandler 应该 当它检测到一个不调用 AllocateRaiseFrame() _TExitDllException 正在引发。

对我来说似乎很清楚,Win64 处理线程关闭的方式存在重大缺陷。这种行为禁止开发任何必须在 Win64 下 27/7 运行的多线程服务器应用程序。

所以:

    您如何看待我的结论? 你们有解决此问题的方法吗?

QC Report 105559

【问题讨论】:

“你们有解决这个问题的办法吗?”我会使用 32 位应用程序,直到下一个带有 64 位编译器的 stable 版本的 delphi 出现。 .. 如果我是你,我会将其缩减为最小尺寸的样本,显示泄漏,然后简单地将其提交给 QC。 @whosrdaddy 也有同样的感觉,我希望我们错了): 很有趣,似乎在 2009 年已经报告了相同类型的错误(我想它是在 Win32 RTL 中):embarcadero.newsgroups.archived.at/public.delphi.rtl/200903/…qc.embarcadero.com/wc/qcmain.aspx?d=72439 不过它似乎已经修复现在因为我的测试项目的 Win32 版本不会泄漏内存。 @Cœur 这是一个自动脚本,它只执行我告诉它去做的事情。我猜剩余的 imageshack.us 链接没有被检测为图像,我不确定 free.fr 的链接。它本可以在去年 8 月运行。 【参考方案1】:

我使用的是Delphi 10.2.3,所描述的问题似乎仍然存在,至少在以下情况下。

// Remark: My TFooThread is created within the 64 Bit DLL:

procedure TFooThread.Execute;
begin
 while not terminated do
  try
   ReadBlockingFromIndySocket();
   ProcessData();
  except on E:Exception do
   begin
    LogTheException(E.Message);
    // Leave loop and thread
    Abort;
   end
  end;
end;

只要循环/线程离开,这就会泄漏内存。 MadExcept 泄漏报告显示异常对象没有被破坏,在我的情况下,当远程关闭连接时主要是 EIdConnClosedGracefully。问题被发现是 Abort 语句离开循环和线程。泄漏报告中的迹象似乎证明了@RemyLebeau 的观察。在主程序而不是 64 位 DLL 中运行完全相同的代码不会泄漏任何内存。

解决方案:将 Abort 语句与 Exit 交换。

结论:64位DLL中的线程执行函数一定不能有异常(Abort也是异常),否则异常会导致内存泄漏。

至少这对我有用。

【讨论】:

【参考方案2】:

为了避免异常内存泄漏陷阱,您可以尝试在 FoobarProc 周围放置一个 try/except。也许不是为了一个明确的解决方案,而是为了看看为什么首先提出了这个问题。

我通常是这样的:

try
  FooBarProc()
except
  if IsFatalException(ExceptObject) then // checks for system exceptions like AV, invalidop etc
    OutputDebugstring(PChar(ExceptionToString(ExceptObject))) // or some other way of logging
end;

【讨论】:

【参考方案3】:

一个非常简单的解决方法是重用线程而不是创建和销毁它们。线程非常昂贵,您可能还会获得性能提升...不过,对调试表示赞赏...

【讨论】:

是的,这是我的第一个想法。在我的具体情况下,我当然可以使用线程池。但这不会阻止我的项目中包含的第三方代码来安排也会泄漏的新线程...... 没错,但是如果您有第三方资源的源代码,则可以对其进行调整以使用您的线程池,如果没有,您将无能为力无论如何...如果您绝对必须运行外部泄漏的 dll,您应该在一个单独的进程中执行它,您可以不时重新启动,但这并不总是可能的。

以上是关于线程关闭期间Win64 Delphi RTL中的内存泄漏?的主要内容,如果未能解决你的问题,请参考以下文章

Delphi XE10 RTL - PPL - TTask

急!!!Delphi7无法启动!!!

Agilent.GoldenGate.RFIC.Simulation.2015.01.v4.9.0.Win64 1CD(RTL验证标准)

Delphi和OutputDebugString

Delphi:如何确定应用程序是不是在 Win32 / Win64 下运行并在 64 位上自动启动 64 位版本?

编译器指令 WIN32 和 CPUX86、WIN64 和 CPUX64 在 Delphi 中是不是可以互换?