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

很长一段时间,我注意到我的服务器应用程序的Win64版本泄漏内存。 虽然Win32版本在相对稳定的内存占用情况下工作正常,但是64位版本使用的内存有规律地增加 – 也许20Mb /天,没有任何明显的原因(不用说,FastMM4没有报告任何内存泄漏) 。 源代码在32位和64位版本之间是相同的。 该应用程序是围绕Indy TIdTCPServer组件构build的,它是一个连接到数据库的高度multithreading服务器,该数据库处理由Delphi XE2制作的其他客户端发送的命令。

我花了很多时间审查我自己的代码,并试图了解为什么64位版本泄漏了这么多的内存。 我最终通过使用MS工具来追踪DebugDiag和XPerf之类的内存泄漏,似乎在Delphi 64位RTL中存在一个根本的缺陷,每当线程从DLL中分离出来,都会导致一些字节被泄漏。 此问题对于必须全天候运行而不重新启动的高度multithreading应用程序尤其关键。

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

这里是库的源代码:

library FooBarDLL; uses Windows, System.SysUtils, System.Classes; {$R *.res} function FooBarProc(): Boolean; stdcall; begin Result := True; //Do nothing. end; exports FooBarProc; 

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

  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个字节,这是完全可以接受的:

32位版本的内存使用情况http://img401.imageshack.us/img401/6159/soleak32.png

64位二进制显示增加了12476字节(从820K到13296K),这是更有问题的:

64位版本的内存使用情况http://img12.imageshack.us/img12/209/soleak64.png

堆内存的不断增加也由XPerf确认:

XPerf用法http://desmond.imageshack.us/Himg825/scaled.php?server=825&filename=soxperf.png&res=landing

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

 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 帮助我在Embarcadero论坛上了解发生了什么事情:

第二次泄漏看起来更像是一个确定的错误。 在线程closures期间,调用StartLib()函数,调用ExitThreadTLS()释放调用线程的TLS内存块,然后调用Halt0()调用ExitDll()以引发DelphiExceptionHandler()捕获的exception,以调用AllocateRaiseFrame ),它在访问名为ExceptionObjectCount的threadvarvariables时间接调用GetTls()和InitThreadTLS()。 即重新分配正在closures的调用线程的TLS内存块。 因此,在DLL_THREAD_DETACH期间,StartLib()不应该调用Halt0(),或者DelphiExceptionHandler在检测到_TExitDllExceptionexception时不应调用AllocateRaiseFrame()。

在我看来,Win64方式处理线程closures存在一个主要缺陷。 这种行为禁止开发Win64下必须运行27/7的multithreading服务器应用程序。

所以:

  1. 你怎么看待我的结论?
  2. 你们有没有解决这个问题的方法?

testing源代码和二进制文件可以在这里下载 。

感谢您的贡献!

编辑 : QC报告105559 。 我在等你的投票:-)

一个非常简单的解决方法是重新使用线程,而不是创build和销毁它们。 线程是非常昂贵的,你可能会得到一个性能提升…在debugging虽然荣誉…