可能强制Delphi的threadvar内存被释放?
我一直在追查在Delphi 2007 for Win32中构建的DLL中看起来是内存泄漏的东西。 如果线程在卸载DLL时仍然存在(当卸载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;
谢谢。
正如您已经确定的那样,线程本地存储将从每个脱离DLL的线程中释放。 这发生在System._StartLib
当Reason
是DLL_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;
如果您需要从宿主应用程序而不是从DLL内部执行此操作,那么您需要导出一个返回DLL的TlsIndex
值的函数。 这样,主机程序可以在DLL消失后释放存储本身(从而保证在给定线程中不会执行更多的DLL代码)。
请注意,在“帮助”中明确指出,您必须注意释放自己的线程。
一旦你知道你不再需要它们,你应该这样做。
从帮助:
通常由编译器管理的动态变量(长字符串,宽字符串,动态数组,变体和接口)可以用threadvar声明,但编译器不会自动释放由每个执行线程创建的堆分配内存。 如果在线程变量中使用这些数据类型, 则在线程终止之前,您有责任在线程中处理其内存 。 例如,
threadvar S: AnsiString;
S := 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
...
S := ''; // free the memory used by S
注意:不鼓励使用这种结构。
您可以通过将其设置为“未分配”以及将接口或动态数组设置为零来释放变体。
冒着太多代码的风险,这是对我自己的问题的一种可能的(差)解决方案。 使用线程局部存储器存储在threadvar变量的单个块中的事实(正如Kennedy先生指出的那样 - 谢谢),此代码将分配的指针存储在TList中,然后在进程分离时释放它们。 我写的主要只是看它是否会起作用。 我可能不会在生产代码中使用它,因为它会假设有关Delphi运行时可能会因不同版本而改变,甚至在我使用的版本(Delphi 7和2007)中也可能会漏掉问题。
这个实现确实让umdh高兴,它不认为有更多的内存泄漏。 但是,如果我在一个循环中运行测试(加载,调用另一个线程上的入口点,卸载),Process Explorer中看到的内存使用率仍然快速增长。 事实上,我创建了一个完全空的DLL,只有一个空的DllMain(因为我没有将德尔福的全局DllMain指针分配给它......德里本身提供了真正的DllMain入口点,所以没有调用它)。 加载/卸载DLL的简单循环仍然每次迭代泄露4K。 因此,Delphi DLL可能还包含其他内容(原始问题的主要观点)。 但我不知道它是什么。 用C编写的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.
链接地址: http://www.djcxy.com/p/16163.html