Утечка памяти в Win64 Delphi RTL во время закрытия потока?

В течение длительного времени я заметил, что версия Win64 моего серверного приложения дает утечку памяти. В то время как версия Win32 работает нормально с относительно стабильным объемом памяти, память, используемая 64-битной версией, регулярно увеличивается — может быть, 20 МБ / день без какой-либо видимой причины (излишне говорить, что FastMM4 не сообщал об утечке памяти для обоих из них). .Исходный код идентичен между 32-битной и 64-битной версиями. Приложение построено на основе компонента Indy TIdTCPServer, это многопоточный сервер, подключенный к базе данных, которая обрабатывает команды, отправленные другими клиентами, созданными с помощью Delphi XE2.

Я трачу много времени на анализ собственного кода и пытаюсь понять, почему в 64-битной версии происходит утечка памяти. В итоге я использовал инструменты MS, предназначенные для отслеживания утечек памяти, такие как DebugDiag и XPerf, и, похоже, в 64-битной RTL Delphi есть фундаментальный недостаток, который приводит к утечке некоторых байтов каждый раз, когда поток отсоединяется от DLL. Эта проблема особенно критична для многопоточных приложений, которые должны работать 24/7 без перезапуска.

Я воспроизвел проблему с очень простым проектом, состоящим из хост-приложения и библиотеки, созданных с помощью 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 байт, что вполне приемлемо:

Memory usage for the 32 bit version

64-битный двоичный файл показывает увеличение на 12476 байт (с 820 КБ до 13296 КБ), что более проблематично:

Memory usage for the 64 bit version

Постоянное увеличение кучи памяти также подтверждается XPerf:

Использование XPerf http://desmond.imageshack.us/Himg825/scaled.php?server=825&filename=soxperf.png&res=landing

Используя DebugDiag, я смог увидеть путь кода, который выделял утечку памяти:

LeakTrack+13529
!Sysinit::AllocTlsBuffer+13
!Sysinit::InitThreadTLS+2b
!Sysinit::::GetTls+22
!System::AllocateRaiseFrame+e
!System::DelphiExceptionHandler+342
ntdll!RtlpExecuteHandlerForException+d
ntdll!RtlDispatchException+45a
ntdll!KiUserExceptionDispatch+2e
KERNELBASE!RaiseException+39
!System::::RaiseAtExcept+106
!System::::RaiseExcept+1c
!System::ExitDll+3e
!System::::Halt0+54
!System::::StartLib+123
!Sysinit::::InitLib+92
!Smart::initialization+38
ntdll!LdrShutdownThread+155
ntdll!RtlExitUserThread+38
!System::EndThread+20
!System::Classes::ThreadProc+9a
!SystemThreadWrapper+36
kernel32!BaseThreadInitThunk+d
ntdll!RtlUserThreadStart+1d

Реми Лебо помог мне с форумы Embarcadero, чтобы понять, что происходит:

Вторая утечка больше похожа на конкретную ошибку. Во время потока shutdown, вызывается StartLib(), который вызывает ExitThreadTLS() для освобождает блок памяти TLS вызывающего потока, затем вызывает Halt0() для вызовите ExitDll(), чтобы вызвать исключение, которое было перехвачено DelphiExceptionHandler() для вызова AllocateRaiseFrame(), который косвенно вызывает GetTls() и, таким образом, InitThreadTLS() при доступе к переменная threadvar с именем ExceptionObjectCount. Это перераспределяет Блок памяти TLS вызывающего потока, который все еще находится в процессе быть закрытым. Так что либо StartLib() не должен вызывать Halt0() во время DLL_THREAD_DETACH или DelphiExceptionHandler должен не вызывать AllocateRaiseFrame() при обнаружении Возникает исключение _TExitDllException.

Мне кажется очевидным, что в способе Win64 обрабатывать завершение потоков существует серьезный недостаток. Такое поведение запрещает разработку любого многопоточного серверного приложения, которое должно работать 27/7 под Win64.

Итак:

  1. Что вы думаете о моих выводах?
  2. Есть ли у кого-нибудь решение этой проблемы?

Исходный код теста и бинарные файлы можно скачать здесь.

Спасибо за ваш вклад!

Редактировать: Отчет КК 105559. Жду ваших голосов :-)

36
задан Glorfindel 20 August 2019 в 03:12
поделиться