Утечка памяти в Win64 Delphi RTL при отключении потока?
В течение долгого времени я заметил, что версия моего серверного приложения Win64 утечка памяти. В то время как версия Win32 отлично работает с относительно стабильным объемом памяти, объем памяти, используемой 64-битной версией, регулярно увеличивается - возможно, на 20 МБ / день, без каких-либо видимых причин (Излишне говорить, что FastMM4 не сообщал об утечке памяти для них обоих), Исходный код идентичен между 32-битной и 64-битной версией. Приложение построено на компоненте Indy TIdTCPServer, это многопоточный сервер, подключенный к базе данных, который обрабатывает команды, отправленные другими клиентами, созданными с помощью Delphi XE2.
Я провожу много времени, просматривая свой собственный код и пытаясь понять, почему 64-битная версия вытекла так много памяти. В итоге я использовал инструменты MS, предназначенные для отслеживания утечек памяти, такие как DebugDiag и XPerf, и кажется, что в Delphi 64bit RTL есть фундаментальный недостаток, который вызывает утечку некоторых байтов каждый раз, когда поток отсоединяется от 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 байт, что вполне приемлемо:
http://img401.imageshack.us/img401/6159/soleak32.png
64-разрядный двоичный файл показывает увеличение на 12476 байт (с 820 КБ до 13296 КБ), что более проблематично:
http://img12.imageshack.us/img12/209/soleak64.png
Постоянное увеличение кучи памяти также подтверждается 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(), когда он обращается к переменной threadvar с именем ExceptionObjectCount. Это перераспределяет блок памяти TLS вызывающего потока, который все еще находится в процессе закрытия. Поэтому либо StartLib() не должен вызывать Halt0() во время DLL_THREAD_DETACH, либо DelphiExceptionHandler не должен вызывать AllocateRaiseFrame(), когда он обнаруживает возникшую исключительную ситуацию _TExitDllException.
Мне кажется очевидным, что в Win64 есть серьезный недостаток для обработки потоков. Такое поведение запрещает разработку любого многопоточного серверного приложения, которое должно работать 27/7 под Win64.
Так:
- Что вы думаете о моих выводах?
- У кого-нибудь из вас есть решение этой проблемы?
Исходный код теста и двоичные файлы можно скачать здесь.
Спасибо за ваш вклад!
Изменить: отчет о контроле качества 105559. Я жду ваших голосов:-)
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;
Я использую Delphi 10.2.3, и описанная проблема, похоже, все еще существует, по крайней мере, при следующих обстоятельствах.
// Remark: My TFooThread is created within the 64 Bit DLL:
procedure TFooThread.Execute;
begin
while not terminated do
try
ReadBlockingFromIndySocket();
ProcessData();
except on E:Exception do
begin
LogTheException(E.Message);
// Leave loop and thread
Abort;
end
end;
end;
Это приводит к утечке памяти всякий раз, когда остается цикл / поток. Отчет об утечке MadExcept показывает, что объект исключения не уничтожается, в моем случае в основном это EIdConnClosedGracefully, когда соединение было закрыто удаленно. Было обнаружено, что проблема заключалась в том, что оператор Abort покинул цикл и, следовательно, поток. Указания в отчете об утечке, кажется, подтверждают наблюдения @RemyLebeau. Выполнение того же самого кода в основной программе вместо 64-битной DLL не приводит к утечке памяти.
Решение: замените оператор Abort на Exit.
Заключение: функция выполнения потока в 64-битной DLL не должна оставляться с исключением (Abort также является исключением), иначе исключение вызовет утечку памяти.
По крайней мере, у меня это сработало.