Можно ли заставить Delphi threadvar Memory быть свободным?

Я преследовал то, что кажется утечкой памяти в 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) в основной поток и затем выполняет вызовы в экспортированную функцию на рабочих потоках. Функция, экспортированная из DLL Delphi, присваивает значение целочисленной переменной threadvar и возвращается. Затем программа C выгружает DLL (через FreeLibrary в основной поток) и повторяет. После примерно 32 000 итераций использование памяти процесса, отображаемое в Process Explorer, превышает 130 МБ. Я также более точно проверил его с помощью umdh. UMDH показал 24 байта, потерянных для каждого экземпляра. Но 130MB в Process Explorer, по-видимому, указывает около 4K на итерацию; Я предполагаю, что 4K-сегмент был утечка каждый раз на основе этого, но я не знаю точно.

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

threadvar
   threadint : integer;

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

Спасибо.

Ответ 1

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

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

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

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 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 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 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 DLL_PROCESS_ATTACH
   gListSync := TCriticalSection.Create;
end.

Ответ 2

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

Предполагается, что DLL получает уведомления об отключении потока. На самом деле, модель, предложенная Microsoft в описании того, как использовать поточно-локальное хранилище с DLL.

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

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

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

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

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

Ответ 3

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

Из справки:

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

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

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