我一直在追逐在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;
感谢。
答案 0 :(得分:6)
正如您已经确定的那样,对于从DLL中分离的每个线程,将发布线程局部存储 。当System._StartLib
为Reason
时,DLL_Thread_Detach
会发生这种情况。但是,为了实现这一点,线程需要终止。线程终止时发生线程分离通知,而不是在卸载DLL时发生。 (如果是相反的情况,操作系统必须在某个地方中断线程,以便它可以代表线程插入DllMain
的调用。这将是灾难性的。)
DLL 应该接收线程分离通知。实际上,这是微软在how to use thread-local storage with DLLs的描述中建议的模型。
释放线程本地存储的唯一方法是从要释放其存储空间的线程的上下文中调用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代码)。
答案 1 :(得分:3)
请注意,在“帮助”中明确指出,您必须自己解决线程变形问题 一旦你知道你不再需要它们,你应该这样做。
来自帮助:
通常由编译器管理的动态变量(长字符串,宽字符串,动态数组,变体和接口)可以使用threadvar声明,但编译器不会自动释放由堆创建的堆分配的内存每个执行线程。如果在线程变量中使用这些数据类型,在线程终止之前,您有责任从线程内部处理内存。例如,
threadvar S: AnsiString;
S := 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
...
S := ''; // free the memory used by S
注意:不鼓励使用此类构造。
您可以通过将变量设置为“未分配”以及通过将其设置为nil来设置接口或动态数组来释放变体。
答案 2 :(得分:3)
冒着太多代码的风险,这是我自己的问题可能(糟糕)的解决方案。使用线程局部存储内存存储在threadvar变量的单个块中的事实(正如肯尼迪先生所指出的那样 - 感谢),此代码将分配的指针存储在TList中,然后在进程分离时释放它们。我写它主要是为了看看它是否有用。我可能不会在生产代码中使用它,因为它假设Delphi运行时可能会随着不同的版本而改变,甚至可能会错过我正在使用的版本(Delphi 7和2007)的问题。
这个实现确实让umdh高兴,它认为没有更多的内存泄漏。但是,如果我在循环中运行测试(加载,在另一个线程上调用入口点,卸载),则在Process Explorer中看到的内存使用量仍然会快速增长。实际上,我创建了一个完全空的DLL,只有一个空的DllMain(由于我没有将Delphi的全局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.