线程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服务器应用程序。
所以:
- 你怎么看待我的结论?
- 你们有没有解决这个问题的方法?
testing源代码和二进制文件可以在这里下载 。
感谢您的贡献!
编辑 : QC报告105559 。 我在等你的投票:-)
一个非常简单的解决方法是重新使用线程,而不是创build和销毁它们。 线程是非常昂贵的,你可能会得到一个性能提升…在debugging虽然荣誉…