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

35

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

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

Использование памяти для 32-разрядной версии http://img401.imageshack.us/img401/6159/soleak32.png

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

Использование памяти для 64-разрядной версии http://img12.imageshack.us/img12/209/soleak64.png

Постоянное увеличение памяти кучи также подтверждается 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

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

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

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

Итак:

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

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

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

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

  • 3
    «У кого-нибудь из вас есть решение этой проблемы?» Я бы использовал 32-битное приложение, пока не выйдет следующий <too-strong> стабильный </ too-strong> выпуск delphi с 64-битным компилятором ...
  • 4
    На вашем месте я бы сократил образец до минимального размера, который показывает утечку, и просто отправил бы его в КК.
Показать ещё 7 комментариев
Теги:
memory-leaks
win64
delphi-xe2

1 ответ

2

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

  • 0
    Да, это была моя самая первая идея. В моем конкретном случае я, конечно, мог бы использовать пул потоков. Но это не помешает стороннему коду, включенному в мой проект, планировать новые потоки, которые также будут просачиваться ...
  • 0
    Это правда, но если у вас есть источник для ваших сторонних материалов, вы можете адаптировать его для использования вашего пула потоков, если вы этого не сделаете, вы ничего не сможете с этим поделать ... Если вы абсолютно необходимо запускать внешние утечки DLL, вы должны сделать это в отдельном процессе, который вы можете запускать время от времени, но это не всегда возможно.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню