简体   繁体   English

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

[英]Memory leak in the Win64 Delphi RTL during thread shutdown?

For a long time I've noticed that the Win64 version of my server application leak memory.很长一段时间以来,我注意到我的服务器应用程序的 Win64 版本会泄漏内存。 While the Win32 version works fine with a relatively stable memory footprint, the memory used by the 64 bit version increases regularly – maybe 20Mb/day, without any apparent reason (Needless to say, FastMM4 did not report any memory leak for both of them).虽然 Win32 版本运行良好,内存占用相对稳定,但 64 位版本使用的内存有规律地增加——可能 20Mb/天,没有任何明显的原因(不用说,FastMM4 没有报告它们两者的任何内存泄漏) . The source code is identical between the 32bit and the 64bit version. 32 位和 64 位版本的源代码是相同的。 The application is built around the Indy TIdTCPServer component, it is a highly multithreaded server connected to a database that processes commands sent by other clients made with Delphi XE2.该应用程序是围绕 Indy TIdTCPServer 组件构建的,它是一个高度多线程的服务器,连接到一个数据库,用于处理由 Delphi XE2 制作的其他客户端发送的命令。

I spend a lot of time reviewing my own code and trying to understand why the 64 bit version leaked so much memory.我花了很多时间检查自己的代码并试图理解为什么 64 位版本会泄漏这么多内存。 I ended up by using MS tools designed to track memory leaks like DebugDiag and XPerf and it seems there is a fundamental flaw in the Delphi 64bit RTL that causes some bytes to be leaked each time a thread has detached from a DLL.我最终使用了专为跟踪内存泄漏而设计的 MS 工具,例如 DebugDiag 和 XPerf,似乎 Delphi 64 位 RTL 中存在一个基本缺陷,每次线程与 DLL 分离时都会导致一些字节泄漏。 This issue is particularly critical for highly multithreaded applications that must run 24/7 without being restarted.对于必须 24/7 全天候运行而无需重新启动的高度多线程应用程序,此问题尤其重要。

I reproduced the problem with a very basic project that is composed by an host application and a library, both built with XE2.我用一个非常基本的项目重现了这个问题,该项目由一个主机应用程序和一个库组成,两者都用 XE2 构建。 The DLL is statically linked with the host app. DLL 与主机应用程序静态链接。 The host app creates threads that just call the dummy exported procedure and exit:主机应用程序创建的线程只调用虚拟导出过程并退出:

Here is the source code of the library:这是库的源代码:

library FooBarDLL;

uses
  Windows,
  System.SysUtils,
  System.Classes;

{$R *.res}

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

exports
  FooBarProc;

The host application uses a timer to create a thread that just call the exported procedure:宿主应用程序使用计时器创建一个只调用导出过程的线程:

  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;

Here is some screenshots that show the leak using VMMap (look at the red line named "Heap").这是一些使用 VMMap 显示泄漏的屏幕截图(查看名为“Heap”的红线)。 The following screenshots were taken within a 30 minutes interval.以下屏幕截图是在 30 分钟间隔内拍摄的。

The 32 bit binary shows an increase of 16 bytes, which is totally acceptable: 32位二进制显示增加了16个字节,这是完全可以接受的:

32 位版本的内存使用情况

The 64 bit binary shows an increase of 12476 bytes (from 820K to 13296K), which is more problematic: 64位二进制显示增加了12476字节(从820K到13296K),问题比较多:

64 位版本的内存使用情况

The constant increase of heap memory is also confirmed by XPerf:堆内存的不断增加也得到了 XPerf 的证实:

XPerf usage XPerf 使用

Using DebugDiag I was able to see the code path that was allocating the leaked memory:使用 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 to understand what was happening: Remy Lebeau 在 Embarcadero 论坛上帮助我了解正在发生的事情:

The second leak looks more like a definite bug.第二次泄漏看起来更像是一个明确的错误。 During thread shutdown, StartLib() is being called, which calls ExitThreadTLS() to free the calling thread's TLS memory block, then calls Halt0() to call ExitDll() to raise an exception that is caught by DelphiExceptionHandler() to call AllocateRaiseFrame(), which indirectly calls GetTls() and thus InitThreadTLS() when it accesses a threadvar variable named ExceptionObjectCount.在线程关闭期间,StartLib() 被调用,它调用 ExitThreadTLS() 来释放调用线程的 TLS 内存块,然后调用 Halt0() 调用 ExitDll() 来引发异常,该异常被 DelphiExceptionHandler() 捕获到调用 AllocateRaiseFrame( ),它在访问名为 ExceptionObjectCount 的线程变量变量时间接调用 GetTls() 和 InitThreadTLS()。 That re-allocates the TLS memory block of the calling thread that is still in the process of being shut down.这会重新分配仍在关闭过程中的调用线程的 TLS 内存块。 So either StartLib() should not be calling Halt0() during DLL_THREAD_DETACH, or DelphiExceptionHandler should not be calling AllocateRaiseFrame() when it detects a _TExitDllException being raised.因此,StartLib() 不应在 DLL_THREAD_DETACH 期间调用 Halt0(),或者 DelphiExceptionHandler 在检测到引发 _TExitDllException 时不应调用 AllocateRaiseFrame()。

It seems clear for me that there is an major flaw in the Win64 way to handle threads shutdown.对我来说很明显 Win64 处理线程关闭的方式存在一个主要缺陷。 A such behavior prohibits the development of any multithreaded server application that must run 27/7 under Win64.此类行为禁止开发任何必须在 Win64 下 27/7 全天候运行的多线程服务器应用程序。

So:所以:

  1. What do you think of my conclusions?你怎么看我的结论?
  2. Do any of you have a workaround for this issue?你们中有人有解决此问题的方法吗?

QC Report 105559 质检报告 105559

A very simple work around is to re-use the thread and not create and destroy them.一个非常简单的解决方法是重用线程而不是创建和销毁它们。 Threads are pretty expensive, you'll probably get a perf boost too... Kudos on the debugging though...线程非常昂贵,您也可能会获得性能提升...不过在调试方面值得称赞...

In order to avoid the exception memoryleak trap, you could try to put an try/except around the FoobarProc.为了避免异常内存泄漏陷阱,您可以尝试在 FoobarProc 周围放置一个 try/except。 Maybe not for a definitive solution, but to see why the axception is raised in the first place.也许不是为了一个明确的解决方案,而是为了看看为什么首先提出了 axception。

I usually have something like this:我通常有这样的事情:

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;

I use Delphi 10.2.3 and the problem described seems to still exist, at least under the following circumstances.我使用的是 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;

This leaks memory whenever the loop/thread is left.只要离开循环/线程,就会泄漏内存。 MadExcept leak report shows that an exception object is not destroyed, in my case mostly an EIdConnClosedGracefully when the connection was closed remotely. MadExcept 泄漏报告显示异常对象没有被销毁,在我的情况下,当连接远程关闭时主要是 EIdConnClosedGracefully。 The problem was found to be the Abort statement to leave the loop and thus the thread.发现问题出在 Abort 语句离开循环和线程。 Indications in the leak report seem to proof the observations of @RemyLebeau.泄漏报告中的迹象似乎证明了@RemyLebeau 的观察结果。 Running the exact same code in the main program instead of the 64 Bit DLL does not leak any memory.在主程序中运行完全相同的代码而不是 64 位 DLL 不会泄漏任何内存。

Solution: Exchange the Abort statement with Exit.解决方案:将 Abort 语句与 Exit 交换。

Conclusion: A thread execution function in a 64 Bit DLL must not be left with an exception (Abort is an exception as well), or else the exception causes a memory leak.结论:64 位 DLL 中的线程执行函数一定不能留下异常(Abort 也是异常),否则异常会导致内存泄漏。

At least this worked for me.至少这对我有用。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM