В течение долгого времени я заметил, что версия моего серверного приложения 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 байт, что вполне допустимо:
64-разрядный двоичный файл показывает увеличение на 12476 байт (с 820 КБ до 13296 КБ), что более проблематично:
Постоянное увеличение кучи памяти также подтверждается 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
Реми Лебо помог мне на форумах Embarcadero понять, что происходит:
Вторая утечка больше похожа на явную ошибку. Во время потока выключение, вызывается StartLib(), который вызывает ExitThreadTLS() для освободите блок памяти TLS вызывающего потока, затем вызовите Halt0() для вызовите ExitDll(), чтобы вызвать исключение, которое перехватывается DelphiExceptionHandler() для вызова AllocateRaiseFrame(), который косвенно вызывает GetTls() и, следовательно, InitThreadTLS(), когда он обращается к переменная потока с именем ExceptionObjectCount. Это перераспределяет Блок памяти TLS вызывающего потока, который все еще находится в процессе быть закрытым. Так что либо StartLib() не должен вызывать Halt0() во время DLL_THREAD_DETACH или DelphiExceptionHandler должны не вызывать AllocateRaiseFrame(), когда он обнаруживает _TExitDllException повышается.
Мне кажется очевидным, что в Win64 есть серьезный недостаток для обработки потоков. Такое поведение запрещает разработку любого многопоточного серверного приложения, которое должно работать 27/7 под Win64.
Итак:
- Что вы думаете о моих выводах?
- У кого-нибудь из вас есть решение этой проблемы?
Тестовый исходный код и двоичные файлы можно загрузить здесь.
Спасибо за ваш вклад!
Изменить: Отчет о контроле качества 105559. Я жду ваших голосов :-)