長い間、サーバー アプリケーションの Win64 バージョンでメモリ リークが発生することに気付きました。Win32 バージョンは比較的安定したメモリ フットプリントで正常に動作しますが、64 ビット バージョンで使用されるメモリは定期的に増加します。明らかな理由もなく、おそらく 1 日あたり 20Mb 増加します (言うまでもなく、FastMM4 は両方のメモリ リークを報告しませんでした)。 . ソースコードは32bit版と64bit版で同じです。このアプリケーションは、Indy TIdTCPServer コンポーネントを中心に構築されています。これは、Delphi XE2 で作成された他のクライアントから送信されたコマンドを処理するデータベースに接続された高度にマルチスレッド化されたサーバーです。
私は自分のコードを見直して、64 ビット バージョンで大量のメモリ リークが発生した理由を理解しようと多くの時間を費やしています。DebugDiag や XPerf などのメモリ リークを追跡するように設計された MS ツールを使用することになりました。Delphi 64 ビット RTL には、スレッドが DLL から切り離されるたびに一部のバイトがリークされる根本的な欠陥があるようです。この問題は、再起動せずに 24 時間年中無休で実行する必要がある高度にマルチスレッド化されたアプリケーションにとって特に重要です。
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 を使用したリークを示すスクリーンショットを次に示します (「ヒープ」という名前の赤い線を見てください)。次のスクリーンショットは、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
Remy Lebeauは、Embarcadero フォーラムで何が起こっているのかを理解するのを助けてくれました。
2 番目のリークは、明確なバグのように見えます。スレッドのシャットダウン中に、StartLib() が呼び出され、ExitThreadTLS() を呼び出して呼び出しスレッドの TLS メモリ ブロックを解放し、次に Halt0() を呼び出して ExitDll() を呼び出して例外を発生させ、それを DelphiExceptionHandler() がキャッチして AllocateRaiseFrame(これは、間接的に GetTls() を呼び出し、したがって、ExceptionObjectCount という名前の threadvar 変数にアクセスするときに InitThreadTLS() を呼び出します。これにより、まだシャットダウン中の呼び出しスレッドの TLS メモリ ブロックが再割り当てされます。そのため、DLL_THREAD_DETACH 中に StartLib() が Halt0() を呼び出したり、_TExitDllException の発生を検出したときに DelphiExceptionHandler が AllocateRaiseFrame() を呼び出したりしてはなりません。
スレッドのシャットダウンを処理する Win64 の方法に重大な欠陥があることは明らかです。このような動作は、Win64 で 27 時間年中無休で実行する必要があるマルチスレッド サーバー アプリケーションの開発を妨げます。
そう:
- 私の結論についてどう思いますか?
- この問題の回避策はありますか?