首页 文章

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

提问于
浏览
36

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

我花了很多时间来查看自己的代码并试图理解为什么64位版本泄露了如此多的内存 . 最后我使用了专门用于跟踪DebugDiag和XPerf等内存泄漏的MS工具,看起来Delphi 64位RTL中存在一个基本缺陷,每次线程从DLL分离时都会导致某些字节泄露 . 对于必须在不重新启动的情况下全天候运行的高度多线程应用程序,此问题尤其重要 .

我用一个由主机应用程序和库组成的非常基本的项目重现了这个问题,这两个项目都是用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个字节,这是完全可以接受的:

Memory usage for the 32 bit version http://img401.imageshack.us/img401/6159/soleak32.png

64位二进制文件显示增加了12476个字节(从820K到13296K),这更成问题:

Memory usage for the 64 bit version http://img12.imageshack.us/img12/209/soleak64.png

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

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

使用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( ),当它访问名为ExceptionObjectCount的threadvar变量时,间接调用GetTls(),从而调用InitThreadTLS() . 这会重新分配仍处于关闭状态的调用线程的TLS内存块 . 因此,在DLL_THREAD_DETACH期间,StartLib()不应该调用Halt0(),或者DelphiExceptionHandler在检测到引发_TExitDllException时不应该调用AllocateRaiseFrame() .

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

所以:

  • 您如何看待我的结论?

  • 你们有没有解决这个问题的方法?

测试源代码和二进制文件can be downloaded here .

谢谢你的贡献!

EditQC Report 105559 . 我在等你的投票:-)

2 回答

  • 0

    一个非常简单的解决方法是重用线程而不是创建和销毁它们 . 线程非常昂贵,你可能也会得到一个性能提升...虽然在调试方面感到荣幸......

  • 2

    为了避免异常memoryleak陷阱,你可以试着在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;
    

相关问题