В течение долгого времени 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:
Используя 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. Я жду ваших голосов: -)
Очень простая работа заключается в том, чтобы повторно использовать поток, а не создавать и уничтожать их. Потоки довольно дороги, вы, вероятно, получите импульс повышения... Престижность в отладке, хотя...