Можно ли принудительно освободить память Delphi threadvar?

Я искал утечку памяти в DLL, построенной в Delphi 2007 для Win32. Память для переменных threadvar не освобождается, если потоки все еще существуют, когда DLL выгружается (нет активных вызовов DLL, когда она выгружается).

Вопрос: есть ли способ заставить Delphi освобождать память, связанную с переменными threadvar? Это не так просто, как просто не использовать их. Некоторые из существующих компонентов Delphi используют их, поэтому даже если DLL не объявляет их явно, она в конечном итоге их использует.

Немного подробностей. Я отследил это до вызова LocalAlloc, который происходит в ответ на использование переменной threadvar, которая является «оболочкой» Delphi для локального хранилища потока в Win32. Для любопытных: вызов выделения находится в исходном файле Delphi sysinit.pas. Соответствующий вызов LocalFree происходит только для потоков, которые получают DLL_THREAD_DETACH вызовов. Если у вас есть несколько потоков в приложении и вы выгружаете DLL, не будет DLL_THREAD_DETACH вызова для каждого потока. DLL получает DLL_PROCESS_DETACH и больше ничего; Я считаю, что это ожидаемо и справедливо. Таким образом, происходит утечка любых выделений локальной памяти для других потоков.

Я воссоздал его с помощью короткой программы на C, которая запускает несколько «рабочих» потоков. Он загружает DLL (через LoadLibrary) в основной поток, а затем выполняет вызовы экспортируемой функции в рабочих потоках. Функция, экспортированная из Delphi DLL, присваивает значение целочисленной переменной threadvar и возвращает. Затем программа на C выгружает DLL (через FreeLibrary в основном потоке) и повторяет. Примерно после 32000 итераций использование памяти процессом, показанное в Process Explorer, вырастает до более 130 МБ. Я также более точно проверил это с помощью umdh. UMDH показал, что на каждый экземпляр потеряно 24 байта. Но 130 МБ в Process Explorer, по-видимому, указывают примерно на 4 КБ на итерацию; Я предполагаю, что каждый раз просочился сегмент 4K, но я не знаю наверняка.

Для пояснения вот объявление threadvar и вся экспортированная функция:

threadvar
   threadint : integer;

function Startup( ulID: LongWord; hValue: Longint ): LongWord; stdcall;
begin
   threadint := 123;
   Result := 0;
end;

Спасибо.


person Mark Wilkins    schedule 01.07.2009    source источник


Ответы (3)


Как вы уже определили, локальное хранилище потока будет освобождено для каждого потока, который отключается от DLL. Это происходит в System._StartLib, когда Reason равно DLL_Thread_Detach. Однако для этого поток должен завершиться. Уведомления об отсоединении потока возникают при завершении потока, а не при выгрузке библиотеки DLL. (Если бы все было наоборот, ОС пришлось бы где-то прервать поток, чтобы она могла вставить вызов DllMain от имени потока. Это было бы катастрофой.)

Библиотека DLL должна получать уведомления об отсоединении потоков. Фактически, это модель, предложенная Microsoft в описании того, как использовать локальный поток хранилище с DLL.

Единственный способ освободить локальное хранилище потока - вызвать TlsFree из контекста потока, хранилище которого вы хотите освободить. Насколько я могу судить, Delphi хранит все свои потоки в одном индексе TLS, заданном переменной TlsIndex в SysInit.pas. Вы можете использовать это значение для вызова TlsFree, когда захотите, но лучше быть уверенным, что больше не будет кода, выполняемого DLL в текущем потоке.

Поскольку вы также хотите освободить память, используемую для хранения всех переменных потоков, вам необходимо вызвать TlsGetValue, чтобы получить адрес буфера, который выделяет Delphi. Вызов LocalFree по этому указателю.

Это будет (непроверенный) код Delphi для освобождения локального хранилища потока.

var
  TlsBuffer: Pointer;
begin
  TlsBuffer := TlsGetValue(SysInit.TlsIndex);
  LocalFree(HLocal(TlsBuffer));
  TlsFree(SysInit.TlsIndex);
end;

Если вам нужно сделать это из хост-приложения, а не из библиотеки DLL, вам нужно будет экспортировать функцию, которая возвращает значение TlsIndex библиотеки DLL. Таким образом, основная программа может освободить само хранилище после того, как DLL исчезнет (таким образом, гарантируя, что в данном потоке больше не будет выполняться код DLL).

person Rob Kennedy    schedule 01.07.2009
comment
Ах да - я не понимал, что это всего лишь один слот TLS на поток. Спасибо что подметил это. Однако я считаю, что это решение потребует выполнения этого вызова в каждом потоке. И, как вы правильно заявили, невозможно / желательно прерывать другие потоки из того, что они делают, чтобы вызвать TlsGetValue, чтобы получить указатель и освободить его. Между прочим, я считаю, что вызов TlsFree действительно происходит при вызове DLL_PROCESS_DETACH. Но полезно знать, что это один слот TLS для каждого потока. Я подумаю над этим. отметка - person Mark Wilkins; 02.07.2009

Обратите внимание, что в справке четко указано, что вы должны позаботиться об освобождении своих потоков-переменных.
Вам следует сделать это, как только вы узнаете, что они вам больше не понадобятся.

Из справки:

Динамические переменные, которые обычно управляются компилятором (длинные строки, широкие строки, динамические массивы, варианты и интерфейсы), могут быть объявлены с помощью threadvar, но компилятор не освобождает автоматически выделенную кучей память, созданную каждым потоком исполнение. Если вы используете эти типы данных в переменных потока, вы обязаны избавиться от их памяти изнутри потока, прежде чем поток завершится. Например,

threadvar S: AnsiString;
S := 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
  ...
S := '';  // free the memory used by S

Примечание. Использование таких конструкций не рекомендуется.
Вы можете освободить вариант, установив для него значение Unassigned, а для интерфейса или динамического массива - значение nil.

person Francesca    schedule 02.07.2009
comment
В тестовом примере используется 4-байтовое целое число (которое не нужно освобождать); он не использует никаких динамических переменных. Утечка памяти - это память, которую Delphi под крышками выделяет для хранения переменных threadvar. - person Mark Wilkins; 02.07.2009

Рискуя написать слишком много кода, вот возможное (плохое) решение моего собственного вопроса. Используя тот факт, что локальная память потока хранится в одном блоке для переменных threadvar (как указал г-н Кеннеди - спасибо), этот код сохраняет выделенные указатели в TList, а затем освобождает их при отсоединении процесса. Я написал это в основном, чтобы посмотреть, будет ли оно работать. Я, вероятно, не стал бы использовать это в производственном коде, потому что он делает предположения о среде выполнения Delphi, которая может измениться с разными версиями, и, вполне возможно, упускает проблемы даже с версией, которую я использую (Delphi 7 и 2007).

Эта реализация действительно радует umdh, он не думает, что утечек памяти больше. Однако, если я запускаю тест в цикле (загрузка, вызов точки входа в другом потоке, выгрузка), использование памяти, как видно в Process Explorer, по-прежнему растет тревожно быстро. Фактически, я создал полностью пустую DLL, содержащую только пустой DllMain (который не был вызван, поскольку я не назначал ему глобальный указатель DllMain в Delphi ... Сам Дели предоставляет реальную точку входа DllMain). Простой цикл загрузки / выгрузки DLL по-прежнему пропускал 4К за итерацию. Таким образом, все еще может быть что-то еще, что Delphi DLL должна включать (основная мысль исходного вопроса). Но я не знаю, что это такое. DLL, написанная на C, не ведет себя подобным образом.

Наш код (сервер) может вызывать библиотеки DLL, написанные клиентами для расширения функциональности. Обычно мы выгружаем DLL после того, как на нее больше нет ссылок. Я думаю, что моим решением проблемы будет добавление опции, позволяющей оставлять DLL загруженной «постоянно» в памяти. Если клиенты используют Delphi для написания своей DLL, им нужно будет включить эту опцию (или, может быть, мы сможем определить, что это Delphi DLL при загрузке ... необходимо проверить это). Тем не менее, это было интересное упражнение.

library Sample;

uses
  SysUtils,
  Windows,
  Classes,
  HTTPApp,
  SyncObjs;

{$E dll}

var
   gListSync : TCriticalSection;
   gTLSList  : TList;


threadvar
   threadint : integer;


// remove all entries from the TLS storage list
procedure RemoveAndFreeTLS();
var
   i : integer;
begin
   // Only call this at process detach. Those calls are serialized
   // so don't get the critical section.
   if assigned( gTLSList ) then
      for i := 0 to gTLSList.Count - 1 do
         // Is this actually safe in DllMain process detach?  From reading the MSDN
         // docs, it appears that the only safe statement in DllMain is "return;"
         LocalFree( Cardinal( gTLSList.Items[i] ));

end;


// Remove this thread's entry
procedure RemoveThreadTLSEntry();
var
   p : pointer;
begin
   // Find the entry for this thread and remove it.
   gListSync.enter;
   try
      if ( SysInit.TlsIndex <> -1 ) and ( assigned( gTLSList )) then
         begin
            p := TlsGetValue( SysInit.TlsIndex );

            // if this thread didn't actually make a call into the DLL and use a threadvar
            // then there would be no memory for it
            if p <> nil then
               gTLSList.Remove( p );
         end;

   finally
      gListSync.leave;
   end;
end;


// Add current thread's TLS pointer to the global storage list if it is not already
// stored in it.
procedure AddThreadTLSEntry();
var
   p : pointer;
begin
   gListSync.enter;
   try
      // Need to create the list if first call
      if not assigned( gTLSList ) then
         gTLSList := TList.Create;

      if SysInit.TlsIndex <> -1 then
         begin
            p := TlsGetValue( SysInit.TlsIndex );

            if p <> nil then
               begin
               // if it is not stored, add it
               if gTLSList.IndexOf( p ) = -1 then
                  gTLSList.Add( p );
               end;
         end;

   finally
      gListSync.leave;
   end;
end;



// Some entrypoint that uses threadvar (directly or indirectly)
function MyExportedFunc(): LongWord; stdcall;
begin
   threadint := 123;

   // Make sure this thread's TLS pointer is stored in our global list so
   // we can free it at process detach.  Do this AFTER using the threadvar.
   // Delphi seems to allocate the memory on demand.
   AddThreadTLSEntry;
   Result := 0;
end;



procedure DllMain(reason: integer) ;
begin
   case reason of
     DLL_PROCESS_DETACH:
     begin
        // NOTE - if this is being called due to process termination, then it should
        // just return and do nothing.  Very dangerous (and against MSDN recommendations)
        // otherwise.  However, Delphi does not provide that information (the 3rd param of
        // the real DlLMain entrypoint).  In my test, though, I know this is only called
        // as a result of the DLL being unloaded via FreeLibrary
        RemoveAndFreeTLS();
        gListSync.Free;
        if assigned( gTLSList ) then
           gTLSList.Free;
     end;

     DLL_THREAD_DETACH:
        begin
        // on a thread detach, Delphi will clean up its own TLS, so we just
        // need to remove it from the list (otherwise we would get a double free
        // on process detach)
        RemoveThreadTLSEntry();
        end;

   end;
end;




exports
   DllMain,
   MyExportedFunc;


// Initialization
begin
   IsMultiThread := TRUE;

   // Make sure Delphi calls my DllMain
   DllProc := @DllMain;

   // sync object for managing TLS pointers.  Is it safe to create a critical section?
   // This init code is effectively DllMain's DLL_PROCESS_ATTACH
   gListSync := TCriticalSection.Create;
end.
person Mark Wilkins    schedule 03.07.2009