Od dłuższego czasu zauważam, że wersja aplikacji serwera mojej firmy Win64 przecieka. Podczas gdy wersja Win32 działa dobrze z relatywnie stabilnym zapisem pamięci, pamięć używana przez 64-bitową wersję zwiększa się regularnie - może 20 Mb/dobę, bez wyraźnego powodu (nie trzeba dodawać, że FastMM4 nie zgłosił żadnego wycieku pamięci dla nich obu) . Kod źródłowy jest identyczny między wersją 32-bitową a wersją 64-bitową. Aplikacja zbudowana jest wokół komponentu Indy TIdTCPServer, jest to wysoce wielowątkowy serwer podłączony do bazy danych, która przetwarza polecenia wysłane przez innych klientów wykonanych w Delphi XE2.Uszkodzenie pamięci w RTL Win64 Delphi podczas zamykania wątku?
Spędzam dużo czasu przeglądając mój własny kod i próbując zrozumieć, dlaczego wersja 64-bitowa wyciekła tyle pamięci. Skończyło się na użyciu narzędzi MS zaprojektowanych do śledzenia wycieków pamięci, takich jak DebugDiag i XPerf i wydaje się, że istnieje podstawowa luka w 64-bitowym RTL Delphi, która powoduje wyciek niektórych bajtów za każdym razem, gdy wątek odłączy się od biblioteki DLL. Ten problem jest szczególnie krytyczny dla aplikacji wielowątkowych, które muszą działać 24 godziny na dobę, 7 dni w tygodniu, bez ponownego uruchamiania.
Powieliłem problem za pomocą bardzo prostego projektu, który składa się z aplikacji hosta i biblioteki, zarówno zbudowanych z XE2. Biblioteka DLL jest statycznie połączona z aplikacją hosta. Aplikacja hosta tworzy wątki, które po prostu wywołać procedurę atrapę eksportowane i wyjście:
Oto kod źródłowy biblioteki:
library FooBarDLL;
uses
Windows,
System.SysUtils,
System.Classes;
{$R *.res}
function FooBarProc(): Boolean; stdcall;
begin
Result := True; //Do nothing.
end;
exports
FooBarProc;
Aplikacja hosta używa czasomierza stworzyć wątek, który po prostu zadzwonić eksportowany procedura:
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;
Oto kilka zrzutów ekranu pokazujących wyciek za pomocą VMMap (patrz czerwona linia o nazwie "Stóg"). Poniższe zrzuty zostały zrobione w przedziale 30 minut.
32 bitowe binarne wykazuje wzrost 16 bajtów, w którym jest całkowicie do przyjęcia:
Memory usage for the 32 bit version http://img401.imageshack.us/img401/6159/soleak32.png
64-bitowym binarnym wykazuje wzrost 12476 bajtów (od 820K do 13296K), który jest bardziej problematyczne :
Memory usage for the 64 bit version http://img12.imageshack.us/img12/209/soleak64.png
Ciągły wzrost pamięci sterty jest także potwierdzony przez XPerf:
Korzystanie DebugDiag udało mi się zobaczyć ścieżkę kodu, który został przydzielenie wyciekły Pamięć:
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 helped me on the Embarcadero forums aby zrozumieć, co się dzieje:
drugim wygląd szczelności bardziej jak określony błąd. Podczas zamykania wątku jest wywoływana funkcja StartLib(), która wywołuje funkcję ExitThreadTLS() do , zwalnia blok pamięci TLS wywołującego wątku, a następnie wywołuje funkcję Halt0() do wywołanie metody ExitDll() w celu zgłoszenia wyjątku przechwytywanego przez funkcję DelphiExceptionHandler() wywołanie funkcji AllocateRaiseFrame(), która pośrednio wywołuje funkcję GetTls(), a tym samym InitThreadTLS(), gdy uzyskuje dostęp do zmiennej threadvar o numerze o nazwie ExceptionObjectCount.To powoduje ponowne przydzielenie bloku pamięci TLS wątku wywołującego, który wciąż jest w trakcie procesu zamykania. Tak więc albo StartLib() nie powinien wywoływać Halt0() podczas DLL_THREAD_DETACH, albo DelphiExceptionHandler powinien nie wywoływać AllocateRaiseFrame(), gdy wykryje podniesiony _TExitDllException.
Wydaje mi się oczywiste, że istnieje poważna usterka w sposobie zamykania wątków w systemie Win64. Takie zachowanie zabrania rozwoju dowolnej wielowątkowej aplikacji serwerowej, która musi działać 27/7 pod Win64.
Więc:
- Co sądzisz o moich wnioskach?
- Czy ktoś z was ma obejście tego problemu?
Kod źródłowy testu i pliki binarne can be downloaded here.
Dziękujemy za wkład!
Edytuj: QC Report 105559. Czekam na wasze głosy :-)
"Czy ktokolwiek z was ma obejście tego problemu" Korzystałbym z aplikacji 32-bitowej do następnego stabilne wydanie wersja delphi z kompilatorem 64-bitowym pojawia się ... –
ComputerSaysNo
Gdybym był tobą, ograniczyłbym to do próbki o minimalnym rozmiarze, który wykazuje wyciek, i po prostu przesłał go do QC. –
@DorinDuminica: wtedy byłby Delphi XE4;) – whosrdaddy