Possible to force Delphi threadvar Memory to be Freed?
我一直在追逐在Delphi 2007 for Win32中构建的DLL中的内存泄漏。如果在卸载DLL时线程仍然存在,则不会释放threadvar变量的内存(在卸载DLL时没有对DLL进行活动调用)。
问题:有没有办法让Delphi释放与threadvar变量相关的内存?它并不像不使用它们那么简单。许多现有的Delphi组件都使用它们,所以即使DLL没有明确声明它们,它最终也会使用它们。
一些细节
我已将其跟踪到一个LocalAlloc调用,该调用是为了响应threadvar变量的使用而发生的,这是Delphi在Win32中围绕线程局部存储的"包装器"。好奇的是,分配调用是在Delphi源文件sysinit.pas中。只有获得
我用一个简短的C程序重新创建它,启动几个"工作"线程。它在主线程上加载DLL(通过LoadLibrary),然后调用工作线程上的导出函数。从Delphi DLL导出的函数为threadvar整数变量赋值并返回。然后C程序卸载DLL(通过主线程上的FreeLibrary)并重复。在大约32,000次迭代之后,Process Explorer中显示的进程内存使用量增长到130MB以上。我也用umdh更准确地验证了它。 UMDH显示每个实例丢失24个字节。但Process Explorer中的130MB似乎表明每次迭代大约4K;我猜测基于此每次都会泄露4K片段,但我不
为了澄清,这里是threadvar声明和整个导出函数:
1 2 3 4 5 6 7 8 | threadvar threadint : integer; function Startup( ulID: LongWord; hValue: Longint ): LongWord; stdcall; begin threadint := 123; Result := 0; end; |
谢谢。
正如您已经确定的那样,将为每个与DLL分离的线程释放线程本地存储。当
该DLL应该接收线程分离通知。实际上,这是微软在其如何使用DLL的线程局部存储的描述中建议的模型。
释放线程本地存储的唯一方法是从要释放其存储空间的线程的上下文中调用
由于您还想释放用于保存所有threadvars的内存,因此您需要调用
这将是(未经测试的)Delphi代码来释放线程本地存储。
1 2 3 4 5 6 7 | var TlsBuffer: Pointer; begin TlsBuffer := TlsGetValue(SysInit.TlsIndex); LocalFree(HLocal(TlsBuffer)); TlsFree(SysInit.TlsIndex); end; |
如果需要从宿主应用程序而不是在DLL中执行此操作,则需要导出返回DLL的
冒着太多代码的风险,这是我自己的问题可能(糟糕)的解决方案。使用线程局部存储内存存储在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 ...需要检查出来)。尽管如此,这是一项有趣的练习。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | 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. |
请注意,在帮助中明确指出,您必须自己解决你的threadvars。
一旦你知道你不再需要它们,你应该这样做。
来自帮助:
通常由编译器管理的动态变量(长字符串,宽字符串,动态数组,变体和接口)可以使用threadvar声明,但编译器不会自动释放由每个执行线程创建的堆分配的内存。如果在线程变量中使用这些数据类型,则在线程终止之前,您有责任从线程内部处理它们的内存。例如,
1 2 3 4 | threadvar S: AnsiString; S := 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; ... S := ''; // free the memory used by S |
注意:不鼓励使用此类构造。
您可以通过将变量设置为"未分配"以及通过将其设置为nil来设置接口或动态数组来释放变体。