Подтвердить что ты не робот

Утечка памяти в RTL Win64 Delphi при отключении потока?

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

Я провожу много времени, просматривая свой собственный код и пытаясь понять, почему 64-битная версия вытекла так много памяти. В итоге я использовал инструменты MS, предназначенные для отслеживания утечек памяти, такие как DebugDiag и XPerf, и кажется, что в Delphi 64bit 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 (посмотрите на красную линию с именем "Куча"). Следующие снимки экрана были сделаны с интервалом в 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
<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

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

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

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

Итак:

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

Тестовый исходный код и двоичные файлы можно загрузить здесь.

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

Изменить: Отчет о контроле качества 105559. Я жду ваших голосов :-)

4b9b3361

Ответ 1

Очень простая работа заключается в том, чтобы повторно использовать поток, а не создавать и уничтожать их. Потоки довольно дороги, вы, вероятно, получите импульс повышения... Престижность в отладке, хотя...

Ответ 2

Чтобы избежать исключения утечки памяти, вы можете попытаться сделать попытку/исключение вокруг FoobarProc. Может быть, не для окончательного решения, но чтобы понять, почему в первую очередь возникает акцент.

У меня обычно есть что-то вроде этого:

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;