关于多线程:可以强制Delphi threadvar内存被释放?

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中。只有获得DLL_THREAD_DETACH调用的线程才会发生相应的LocalFree调用。如果应用程序中有多个线程并卸载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声明和整个导出函数:

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分离的线程释放线程本地存储。当ReasonDLL_Thread_Detach时,会发生在System._StartLib中。但是,为了实现这一点,线程需要终止。线程终止时发生线程分离通知,而不是在卸载DLL时发生。 (如果是相反的情况,操作系统必须在某个地方中断线程,以便它可以代表线程插入对DllMain的调用。这将是灾难性的。)

该DLL应该接收线程分离通知。实际上,这是微软在其如何使用DLL的线程局部存储的描述中建议的模型。

释放线程本地存储的唯一方法是从要释放其存储空间的线程的上下文中调用TlsFree。据我所知,Delphi将所有的threadvars保存在一个TLS索引中,由SysInit.pas中的TlsIndex变量给出。您可以随时使用该值调用TlsFree,但最好确保当前线程中DLL不再执行任何代码。

由于您还想释放用于保存所有threadvars的内存,因此您需要调用TlsGetValue来获取Delphi分配的缓冲区的地址。在该指针上调用LocalFree

这将是(未经测试的)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的TlsIndex值的函数。这样,主机程序可以在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来设置接口或动态数组来释放变体。