可能强制Delphi的threadvar内存被释放? [英] Possible to force Delphi threadvar Memory to be Freed?

查看:272
本文介绍了可能强制Delphi的threadvar内存被释放?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我一直在追逐在Delphi 2007中为Win32构建的DLL中的内存泄漏。如果在卸载DLL时线程仍然存在,则线程变量的内存不会被释放(卸载DLL时没有活动的调用)。

I have been chasing down what appears to be a memory leak in a DLL built in Delphi 2007 for Win32. The memory for the threadvar variables is not freed if the threads still exist when the DLL is unloaded (there are no active calls into the DLL when it is unloaded).

问题:有没有办法让Delphi释放与threadvar变量相关联的内存?它不像只是不使用它们那么简单。一些现有的Delphi组件使用它们,所以即使DLL没有明确声明它们,它也会使用它们。

The question: Is there some way to cause Delphi to free memory associated with threadvar variables? It is not as simple as just not using them. A number of the existing Delphi components use them, so even if the DLL does not explicitly declare them, it ends up using them.

strong>
我已经将其跟踪到LocalAlloc调用,这是为了响应一个threadvar变量的使用而发生的,这是Delphi在Win32中的线程本地存储周围的包装器。对于好奇,分配调用是在Delphi源文件sysinit.pas中。相应的LocalFree调用仅适用于获取 DLL_THREAD_DETACH 调用的线程。如果应用程序中有多个线程并卸载DLL,则每个线程都没有调用$ code> DLL_THREAD_DETACH 。 DLL得到一个 DLL_PROCESS_DETACH ,没有别的;我相信这是预期和有效的。因此,在其他线程上执行的任何线程本地存储分配都会泄漏。

A Few Details I have tracked it down to a LocalAlloc call that occurs in response to the usage of a threadvar variable, which is Delphi's "wrapper" around thread-local storage in Win32. For the curious, the allocation call is in the Delphi source file sysinit.pas. The corresponding LocalFree call occurs only for threads that get DLL_THREAD_DETACH calls. If you have multiple threads in an application and unload a DLL, there is no DLL_THREAD_DETACH call for each thread. The DLL gets a DLL_PROCESS_DETACH and nothing else; I believe that is expected and valid. Thus, any thread-local storage allocations made on other threads are leaked.

我用一个启动几个工作线程的短C程序重新创建它。它通过主线程加载DLL(通过LoadLibrary),然后在工作线程上调用导出的函数。从Delphi DLL导出的函数将一个值分配给一个threadvar整数变量并返回。 C程序然后卸载DLL(通过主线程上的FreeLibrary)并重复。大约32,000次迭代后,Process Explorer中显示的进程内存使用量增加到超过130MB。我也用umdh更准确地验证了它。 UMDH显示每个实例丢失24个字节。但是,进程资源管理器中的130MB似乎表示每次迭代约4K;我猜测每个4K段都是基于此漏洞,但我不知道。

I re-created it with a short C program that starts several "worker" threads. It loads the DLL (via LoadLibrary) on the main thread and then makes calls into an exported function on the worker threads. The function exported from the Delphi DLL assigns a value to a threadvar integer variable and returns. The C program then unloads the DLL (via FreeLibrary on the main thread) and repeats. After about 32,000 iterations, the process memory usage shown in Process Explorer grows to over 130MB. I also verified it more accurately with umdh. UMDH showed 24 bytes lost per instance. But the 130MB in Process Explorer seems to indicate about 4K per iteration; I'm guessing a 4K segment was leaked each time based on that, but I don't know for sure.

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

For clarification, here is the threadvar declaration and the entire exported function:

threadvar
   threadint : integer;

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

谢谢。

推荐答案

如果有太多代码的风险,这里是一个可能的(差的)解决我自己的问题。使用线程本地存储存储器存储在线程变量的单个块中(如肯尼迪先生指出的那样),该代码将分配的指针存储在TList中,然后在进程分离时释放它们。我写的主要只是为了看看它是否会起作用。我可能不会在生产代码中使用它,因为它使得关于Delphi运行时的假设可能会随着不同的版本而改变,甚至在我使用的版本(Delphi 7和2007)中也可能会丢失问题。

At the risk of way too much code, here is a possible (poor) solution to my own question. Using the fact that the thread-local storage memory is stored in a single block for the threadvar variables (as pointed out by Mr. Kennedy - thanks), this code stores the allocated pointers in a TList and then frees them at process detach. I wrote it mostly just to see if it would work. I probably would not use this in production code because it makes assumptions about the Delphi runtime that could change with different versions and quite possibly misses problems even with the version I am using (Delphi 7 and 2007).

这个实现确实使umdh快乐,它不认为有更多的内存泄漏。但是,如果我在循环中运行测试(加载,在另一个线程上调用entrypoint,卸载),Process Explorer中看到的内存使用情况仍然快速增长。事实上,我创建了一个完全空的DLL,只有一个空的DllMain(没有调用,因为我没有分配Delphi的全局DllMain指针... Delhi本身提供了真正的DllMain入口点)。加载/卸载DLL的简单循环仍然每次迭代4K。所以可能还有一些Delphi DLL应该包含(原始问题的要点)。但我不知道是什么。用C编写的DLL不会这样做。

This implementation does make umdh happy, it doesn't think there are any more memory leaks. However, if I run the test in a loop (load, call entrypoint on another thread, unload), the memory usage as seen in Process Explorer still grows alarmingly fast. In fact, I created a completely empty DLL with only an empty DllMain (that was not called since I did not assign Delphi's global DllMain pointer to it ... Delhi itself provides the real DllMain entrypoint). A simple loop of loading/unloading the DLL still leaked 4K per iteration. So there may still be something else a Delphi DLL is supposed to include (the main point of the original question). But I don't know what it is. A DLL written in C does not behave this way.

我们的代码(服务器)可以调用客户编写的DLL来扩展功能。我们通常在没有更多的引用之后卸载该DLL。我认为我解决这个问题的方法是添加一个选项,让DLL永久地加载到内存中。如果客户使用Delphi编写他们的DLL,他们将需要打开该选项(或者我们可以检测到它是一个加载的Delphi DLL ...需要检查)。尽管如此,这是一个有趣的练习。

Our code (a server) can call DLLs written by customers to extend functionality. We typically unload the DLL after there are no more references to it. I think my solution to the problem is going to be to add an option to leave the DLL loaded "permanently" in memory. If customers use Delphi to write their DLL, they will need to turn that option on (or maybe we can detect that it is a Delphi DLL on load ... need to check that out). Nonetheless, it has been an interesting exercise.

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.

这篇关于可能强制Delphi的threadvar内存被释放?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆