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)
谢谢.
正如您已经确定的那样,将为每个与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;
Run Code Online (Sandbox Code Playgroud)
如果需要从宿主应用程序而不是DLL中执行此操作,则需要导出返回DLL TlsIndex值的函数.这样,主机程序可以在DLL消失后释放存储本身(从而保证在给定的线程中不再执行DLL代码).
冒着代码太多的风险,这里是我自己的问题的一个可能的(糟糕的)解决方案。利用线程本地存储内存存储在 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)