可以强制Delphi threadvar内存被释放吗?

Mar*_*ins 12 delphi dll multithreading memory-leaks

我一直在追逐在Delphi 2007 for Win32中构建的DLL中的内存泄漏.如果在卸载DLL时线程仍然存在,则不会释放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)并重复.在大约32,000次迭代之后,Process Explorer中显示的进程内存使用量增长到130MB以上.我也用umdh更准确地验证了它.UMDH显示每个实例丢失24个字节.但Process Explorer中的130MB似乎表明每次迭代大约4K; 我猜测基于此每次都会泄露4K片段,但我不确定.

为了澄清,这里是threadvar声明和整个导出函数:

threadvar
   threadint : integer;

function Startup( ulID: LongWord; hValue: Longint ): LongWord; stdcall;
begin
   threadint := 123;
   Result := 0;
end;
Run Code Online (Sandbox Code Playgroud)

谢谢.

Rob*_*edy 6

正如您已经确定的那样,将为每个与DLL分离的线程释放线程本地存储.这发生在System._StartLibReasonDLL_Thread_Detach.但是,为了实现这一点,线程需要终止.线程终止时发生线程分离通知,而不是在卸载DLL时发生.(如果是相反的方式,操作系统必须在某个地方中断线程,以便它可以DllMain代表线程插入一个调用.这将是灾难性的.)

该DLL 应该得到线程分离通知.实际上,这是微软在其如何使用DLL的线程局部存储的描述中建议的模型.

释放线程本地存储的唯一方法是TlsFree从要获取其存储空间的线程的上下文中调用.据我所知,Delphi将所有的threadvars保存在一个TLS索引中,由SysInit.pas中TlsIndex变量给出.您可以随时使用该值进行调用TlsFree,但最好确保当前线程中DLL不再执行任何代码.

由于您还想释放用于保存所有threadvars的内存,因此您需要调用TlsGetValue以获取Delphi分配的缓冲区的地址.调用LocalFree那个指针.

这将是(未经测试的)Delphi代码,用于释放线程本地存储.

var
  TlsBuffer: Pointer;
begin
  TlsBuffer := TlsGetValue(SysInit.TlsIndex);
  LocalFree(HLocal(TlsBuffer));
  TlsFree(SysInit.TlsIndex);
end;
Run Code Online (Sandbox Code Playgroud)

如果需要从宿主应用程序而不是DLL中执行此操作,则需要导出返回DLL TlsIndex值的函数.这样,主机程序可以在DLL消失后释放存储本身(从而保证在给定的线程中不再执行DLL代码).


Mar*_*ins 3

冒着代码太多的风险,这里是我自己的问题的一个可能的(糟糕的)解决方案。利用线程本地存储内存存储在 threadvar 变量的单个块中的事实(正如 Kennedy 先生所指出的 - 谢谢),此代码将分配的指针存储在 TList 中,然后在进程分离时释放它们。我写它主要是为了看看它是否有效。我可能不会在生产代码中使用它,因为它对 Delphi 运行时做出了假设,该运行时可能会随不同版本而变化,并且即使是我正在使用的版本(Delphi 7 和 2007)也很可能会错过问题。

这个实现确实让 umdh 高兴,它认为不再有内存泄漏。但是,如果我在循环中运行测试(加载、调用另一个线程上的入口点、卸载),则 Process Explorer 中看到的内存使用量仍然增长得惊人。事实上,我创建了一个完全空的 DLL,只有一个空的 DllMain(因为我没有将 Delphi 的全局 DllMain 指针分配给它,所以没有调用它......德里本身提供了真正的 DllMain 入口点)。加载/卸载 DLL 的简单循环每次迭代仍会泄漏 4K。因此,Delphi DLL 可能还应该包含其他内容(原始问题的要点)。但我不知道那是什么。用 C 编写的 DLL 不会有这种行为。

我们的代码(服务器)可以调用客户编写的DLL来扩展功能。我们通常会在不再引用 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.
Run Code Online (Sandbox Code Playgroud)