2012-05-11 17 views
35

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:

XPerf usage http://desmond.imageshack.us/Himg825/scaled.php?server=825&filename=soxperf.png&res=landing

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:

  1. Co sądzisz o moich wnioskach?
  2. 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 :-)

+3

"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

+4

Gdybym był tobą, ograniczyłbym to do próbki o minimalnym rozmiarze, który wykazuje wyciek, i po prostu przesłał go do QC. –

+0

@DorinDuminica: wtedy byłby Delphi XE4;) – whosrdaddy

Odpowiedz

2

Bardzo prosta praca polega na ponownym użyciu nici, a nie ich tworzeniu i niszczeniu. Wątki są dość drogie, prawdopodobnie dostaniesz również ulepszenia ... Kudos w debugowaniu ...

+0

Tak, to był mój pierwszy pomysł. W moim konkretnym przypadku mógłbym oczywiście użyć puli wątków. Ale to nie przeszkodzi kodowi stron trzecich zawartemu w moim projekcie do zaplanowania nowych wątków, które również będą przeciekać ... –

+0

To prawda, ale jeśli masz źródło dla rzeczy firm trzecich, możesz go dostosować, aby użyć puli wątków , jeśli tego nie zrobisz, to i tak nie będziesz w stanie nic zrobić ... Jeśli koniecznie musisz uruchomić zewnętrzne nieszczelne biblioteki DLL, powinieneś zrobić to w oddzielnym procesie, który możesz ponownie uruchomić co od czasu do czasu, ale nie zawsze jest to możliwe. – Eric

Powiązane problemy