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

线程关闭期间Win64 Delphi RTL内存泄漏?,delphi,memory-leaks,delphi-xe2,win64,Delphi,Memory Leaks,Delphi Xe2,Win64,很长一段时间以来,我注意到我的服务器应用程序的Win64版本泄漏内存。虽然Win32版本在相对稳定的内存占用下运行良好,但64位版本使用的内存会定期增加–可能是20Mb/天,没有任何明显的原因(不用说,FastMM4没有报告这两个版本的内存泄漏)。32位和64位版本的源代码相同。该应用程序是围绕Indy TIdTCPServer组件构建的,它是一个高度多线程的服务器,连接到一个数据库,该数据库处理由Delphi XE2生成的其他客户端发送的命令 我花了很多时间检查自己的代码,并试图理解为什么64

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

我花了很多时间检查自己的代码,并试图理解为什么64位版本泄漏了这么多内存。我最终使用了MS工具来跟踪内存泄漏,比如DebugDiag和XPerf,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个字节,这是完全可以接受的:

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

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

使用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
LeakTrack+13529
!Sysinit::alloctsbuffer+13
!Sysinit::InitThreadTLS+2b
!Sysinit::GetTls+22
!系统::分配器ISEFRAME+e
!系统::DelphiExceptionHandler+342
ntdll!RtlpExecuteHandlerForException+d
ntdll!RtlDispatchException+45a
ntdll!KiUserExceptionDispatch+2e
内核库!上升异常+39
!系统:::RaiseateExcept+106
!系统::RaiseExcept+1c
!系统::exitdl+3e
!系统:Halt0+54
!系统:::IB+123
!Sysinit:::InitLib+92
!智能::初始化+38
ntdll!LDRSHUTT下螺纹+155
ntdll!RtlExitUserThread+38
!系统::EndThread+20
!系统::类::ThreadProc+9a
!SystemThreadWrapper+36
内核32!BaseThreadInitThunk+d
ntdll!RtlUserThreadStart+1d
Remy Lebeau想了解发生了什么:

第二个漏洞看起来更像是一个明确的bug。线程期间 关机时,正在调用startIB(),这将调用ExitThreadTLS()以 释放调用线程的TLS内存块,然后调用Halt0()以 调用exitdl()以引发被捕获的异常 DelphiExceptionHandler()调用AllocateRaiseFrame(),其中 当它访问 名为ExceptionObjectCount的threadvar变量。这将重新分配资源 仍在进程中的调用线程的TLS内存块 被关闭的危险。因此,任何一个startIB()都不应该调用 DLL_线程_分离期间的Halt0(),或DelphiExceptionHandler应 当检测到错误时,不能调用AllocateRaiseFrame() _正在引发TexitdlLexException

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

因此:

  • 你认为我的结论如何

  • 你们中有人有解决这个问题的办法吗

  • 一个非常简单的解决方法是重新使用线程,而不是创建和销毁线程。线程非常昂贵,您可能也会得到性能提升。。。尽管调试很好…

    为了避免异常memoryleak陷阱,您可以尝试在FoobarProc周围放置一个try/except。也许不是为了一个最终的解决方案,而是为了看看为什么首先提出axception

    我通常有这样的东西:

    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;
    

    我使用Delphi10.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中运行完全相同的代码不会泄漏任何内存

    解决方案:用Exit交换Abort语句

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


    至少这对我是有效的。

    “你们中有人对此问题有解决办法吗”我会使用32位应用程序,直到下一个稳定版本的带有64位编译器的delphi出现……如果我是你,我会将其缩减为最小尺寸的样本,显示泄漏,然后简单地提交给QC。@Who这里有同样的感觉,我希望我们是错的tho):有趣的是,似乎2009年已经报告了一个类似的错误(我想它是在Win32 RTL中):然而,它似乎已经被修复了,因为我的测试项目的Win32版本没有泄漏内存。@Cœur这是一个自动脚本,它只做我告诉它做的事情。我猜剩下的imageshack.us链接没有被检测为图像,我也不确定该链接是否正确