C / C + +运行期库要使用线程本地存储器( T L S)。由于运行期库是在多线程应用程序出现前的许多年设计的,因此运行期库中的大多数函数是用于单线程应用程序的。函数s t r t o k就是个很好的例子。应用程序初次调用s t r t o k时,该函数传递一个字符串的地址,并将字符串的地址保存在它自己的静态变量中。当你将来调用s t r t o k函数并传递N U L L时,该函数就引用保存的字符串地址。
在多线程环境中,一个线程可以调用s t r t o k,然后,在它能够再次调用该函数之前,另一个线程也可以调用S t r t o k。在这种情况下,第二个线程会在第一个线程不知道的情况下,让s t r t o k用一个新地址来改写它的静态变量。第一个线程将来调用s t r t o k时将使用第二个线程的字符串,这就会导致各种各样难以发现和排除的错误。
为了解决这个问题,C / C + +运行期库使用了T L S。每个线程均被赋予它自己的字符串指针,供s t r t o k函数使用。需要予以同样对待的其他C / C + +运行期库函数还有a s c t i m e和g m t i m e。
如果你的应用程序需要严重依赖全局变量或静态变量,那么T L S能够帮助解决它遇到的问题。但是编程人员往往尽可能减少对这些变量的使用,而更多地依赖自动(基于堆栈的)变量和通过函数的参数传递的数据。这样做是很好的,因为基于堆栈的变量总是与特定的线程相联系的。
标准的C运行期库一直是由许多不同的编译器供应商来实现和重新实现的。如果C编译器不包含标准的C运行期库,那么就不值得去购买它。编程员多年来一直使用标准的C运行期库,并且将会继续使用它,这意味着s t r t o k之类的函数的原型和行为特性必须与上面所说的标准C运行期库完全一样。如果今天重新来设计C运行期库,那么它就必须支持多线程应用程序的环境,并且必须采取相应的措施来避免使用全局变量和静态变量。
在我的软件开发项目中,我总是尽可能避免使用全局变量和静态变量。如果你的应用程序使用全局变量和静态变量,那么建议你务必观察每个变量,并且了解一下它能否改变成基于堆栈的变量。如果打算将线程添加给应用程序,那么这样做可以节省大量时间,甚至单线程应用程序也能够从中得到许多好处。
在编写应用程序和D L L时,可以使用本章中介绍的两种T L S方法,即动态T L S和静态T L S。但是,当创建D L L时,这些T L S往往更加有用,因为D L L常常不知道它们链接到的应用程序的结构。不过,当编写应用程序时,你通常知道将要创建多少线程以及如何使用这些线程。然后就可以创造一些临时性的方法,或者最好是使用基于堆栈的方法(局部变量),将数据与创建的每个线程联系起来。不管怎样,应用程序开发人员也能从本章讲述的内容中得到一些启发。
若要使用动态T L S,应用程序可以调用一组4个函数。这些函数实际上是D L L用得最多的函数。图2 1 - 1显示了Wi n d o w s用来管理T L S的内部数据结构。
图21-1 用于管理T L S的内部数据结构
该图显示了系统中运行的线程正在使用的一组标志。每个标志均可设置为F R E E或者I N U S E,表示T L S时隙( s l o t )是否正在使用。M i c r o s o f t保证至少T L S _ M I N I M U M _ AVA I L A B L E位标志是可供使用的。另外, T L S _ M I N I M U M _ AVA I L A B L E在Wi n N T. h中被定义为6 4。Wi n d o w s2 0 0 0将这个标志数组扩展为允许有1 0 0 0个以上的T L S时隙。对于任何一个应用程序来说,这个时隙数量足够了。
若要使用动态T L S,首先必须调用T l s A l l o c函数:
DWORD TlsAlloc();
如果T l s A l l o c在该列表中找不到F R E E标志,它就返回T L S _ O U T _ O F _ I N D E X E S(在Wi n B a s e . h中定义为0 x F F F F F F F F)。当T l s A l l o c第一次被调用时,系统发现第一个标志是F R E E,并将该标志改为I N U S E,同时T l s A l l o c返回0。T l s A l l o c这样运行的概率是9 9 %。下面介绍在另外的1 %的概率下T l s A l l o c是如何运行的。
当创建一个线程时,便分配一个T L S _ M I N I M U M _ AVA I L A B L E P V O I D值的数组,并将它初始化为0,然后由系统将它与线程联系起来。如图2 1 - 1所示,每个线程均得到它自己的数组,数组中的每个P V O I D可以存储任何值。
在能够将信息存储在线程的P V O I D数组中之前,必须知道数组中的哪个索引可供使用,这就是前面调用T l s A l l o c所要达到的目的。按照设计概念, T l s A l l o c为你保留了一个索引。如果T l s A l l o c返回索引3,那么就说明目前在进程中运行的每个线程中均为你保留了索引3,而且在将来创建的线程中也保留了索引3。
若要将一个值放入线程的数组中,可以调用T l s S e t Va l u e函数:
BOOL TlsSetValue( DWORD dwTlsIndex, PVOID pvTlsValue);
线程在调用T l s S e t Va l u e时,可以改变它自己的数组。但是它不能为另一个线程设置T L S值。我希望有另一个T l s函数能够用于使一个线程将数据存储到另一个线程的数组中,但是不存在这样一个函数。目前,将数据从一个线程传递到另一个线程的唯一方法是,将单个值传递给C r e a t e T h r e a d或_ b e g i n t h r e a d e x,然后该函数将该值作为唯一的参数传递给线程的函数。
当调用T l s S e t Va l u e时,始终都应该传递较早的时候调用的T l s A l l o c函数返回的索引。M i c r o s o f t设计的这些函数能够尽快地运行,在运行时,将放弃错误检查。如果传递的索引是调用T l s A l l o c时从未分配的,那么系统将设法把该值存储在线程的数组中,而不进行任何错误检查。
若要从线程的数组中检索一个值,可以调用T l s G e t Va l u e:
PVOID TlsGetValue(DWORD dwTlsIndex);
当在所有线程中不再需要保留T L S时隙的位置的时候,应该调用T l s F r e e:
BOOL TlsFree(DWORD dwTlsIndex);
使用动态T L S
通常情况下,如果D L L使用T L S,那么当它用D L L _ P R O C E S S _ AT TA C H标志调用它的D l l M a i n函数时,它也调用T l s A l l o c。当它用D L L _ P R O C E S S _ D E TA C H调用D l l M a i n函数时,它就调用TlsFree。对TlsSetValue和TlsGetValue的调用很可能是在调用DLL中包含的函数时进行的。
将T L S添加给应用程序的方法之一是在需要它时进行添加。例如,你的D L L中可能有一个运行方式类似s t r t o k的函数。第一次调用这个函数时,线程传递一个指向4 0字节的结构的指针。必须保存这个结构,这样,将来调用函数时就可以引用它。可以像下面这样对你的函数进行编码:
DWORD g_dwTlsIndex; // Assume that this is initialized // with the result of a call to TlsAlloc. void MyFunction(PSOMESTRUCT pSomeStruct) { if (pSomeStruct != NULL) { // The caller is priming this function. // See if we already allocated space to save the data. if (TlsGetValue(g_dwTlsIndex) == NULL) { //Space was never allocated. This is the first //time this function has ever been called by this thread. TlsSetValue(g_dwTlsIndex, HeapAlloc(GetProcessHeap(), 0, sizeof(*pSomeStruct)); } // Memory already exists for the data; // save the newly passed values. memcpy(TlsGetValue(g_dwTlsIndex), pSomeStruct, sizeof(*pSomeStruct)); } else { // The caller already primed the function. Now it // wants to do something with the saved data. // Get the address of the saved data. pSomeStruct = (PSOMESTRUCT) TlsGetValue(g_dwTlsIndex); // The saved data is pointed to by pSomeStruct; use it. ... }
6 4个T L S位置看来超出了你的需要。但是请记住,应用程序可以动态地链接到若干个D L L。第一个D L L可以分配1 0个T L S索引,第二个D L L可以分配5个T L S索引,依此类推。减少你需要的T L S索引始终是个好思路。减少所用T L S索引的最好的办法,是采用前面的代码中的M y F u n c t i o n使用的那种方法。当然,可以在多个T L S索引中保存全部4 0个字节,但是这样做不仅很浪费,而且使数据的操作很困难。相反,应该为数据分配一个内存块,并且像M y F u n c t i o n那样,只将指针保存在单个T L S索引中。正如前面讲过的那样, Windows 2000允许设置1 0 0 0多个T L S时隙。M i c r o s o f t增加了时隙的数量,因为许多编程人员对时隙的使用采取一种只顾自己不顾其他的态度,不给其他D L L分配时隙,从而导致它们运行失败。
前面介绍T l s A l l o c函数时,只描述了该函数能够实现的9 9 %的功能。为了帮助了解剩下的1 %的功能,让我们观察一下下面的代码段:
DWORD dwTlsIndex; PVOID pvSomeValue; ... dwTlsIndex = TlsAlloc(); TlsSetValue(dwTlsIndex, (PVOID)12345); TlsFree(dwTlsIndex); // Assume that the dwTlsIndex value returned from // this call to TlsAlloc is identical to the index // returned by the earlier call to TlsAlloc. dwTlsIndex = TlsAlloc(); pvSomeValue = TlsGetValue(dwTlsIndex);
这是很不错的,因为应用程序可能调用L o a d L i b r a r y来加载D L L,而D L L则调用T l s A l l o c来分配索引,然后,线程调用F r e e L i b r a r y来删除D L L。D L L应该通过调用T l s F r e e来释放它的索引,但是谁知道D L L的代码将哪些值放入线程的数组中呢?接着,线程调用L o a d L i b r a r y,将另一个D L L加载到内存中。该D L L启动时也调用T l s A l l o c,并获得与前面的D L L相同的索引。如果T l s A l l o c没有为进程中的所有线程设置返回的索引,那么线程就可能看到一个老的值,而代码则无法正确地运行。
例如,这个新D L L可以查看对T l s G e t Va l u e函数的调用是否曾经为一个线程分配了内存,就像前面的代码段显示的那样。如果T l s A l l o c没有删除每个线程的数组项目,那么第一个D L L的老数据仍然可以使用。如果线程调用M y F u n c t i o n,那么M y F u n c t i o n就会认为内存块已经分配,并且调用m e m c p y函数,将新数据拷贝到它所认为的内存块中。这可能造成灾难性的后果,不过,幸好T l s A l l o c会对数组元素进行初始化,使这样的灾难永远不会发生。
与动态T L S一样,静态T L S也能够将数据与线程联系起来。但是,静态T L S在代码中使用起来要容易得多,因为不必调用任何函数就能够使用它。
比如说,你想要将起始时间与应用程序创建的每个线程联系起来。只需要将起始时间变量声明为下面的形式:
__declspec(thread) DWORD gt_dwStartTime = 0;
当编译器对程序进行编译时,它将所有的T L S变量放入它们自己的节,这个节的名字是. t l s。 链接程序将来自所有对象模块的所有. t l s节组合起来,形成结果的可执行文件或D L L文件中的 一个大的. t l s节。
为了使静态T L S能够运行,操作系统必须参与其操作。当你的应用程序加载到内存中时,系统要寻找你的可执行文件中的. t l s节,并且动态地分配一个足够大的内存块,以便存放所有的静态T L S变量。你的应用程序中的代码每次引用其中的一个变量时,就要转换为已分配内存块中包含的一个内存位置。因此,编译器必须生成一些辅助代码来引用该静态T L S变量,这将使你的应用程序变得比较大而且运行的速度比较慢。在x86 CPU上,将为每次引用的静态T L S变量生成3个辅助机器指令。
如果在进程中创建了另一个线程,那么系统就要将它捕获并且自动分配另一个内存块,以便存放新线程的静态T L S变量。新线程只拥有对它自己的静态T L S变量的访问权,不能访问属于其他线程的T L S变量。
这就是静态T L S变量如何运行的基本情况。现在让我们来看一看D L L的情况。你的应用程序很可能要使用静态T L S变量,并且链接到也想使用静态T L S变量的一个D L L。当系统加载你的应用程序时,它首先要确定应用程序的. t l s节的大小,并将这个值与你的应用程序链接的D L L中的任何. t l s节的大小相加。当在你的进程中创建线程时,系统自动分配足够大的内存块来存放应用程序需要的所有T L S变量和所有隐含链接的D L L。
下面让我们来看一下当应用程序调用L o a d L i b r a r y,以便链接到也包含静态T L S变量的一个D L L时,将会发生什么情况。系统必须查看进程中已经存在的所有线程,并扩大它们的T L S内存块,以便适应新D L L对内存的需求。另外,如果调用F r e e L i b r a r y来释放包含静态T L S变量的D L L,那么与进程中的每个线程相关的的内存块应该被压缩。
对于操作系统来说,这样的管理任务太重了。虽然系统允许包含静态T L S变量的库在运行期进行显式加载,但是T L S数据没有进行相应的初始化。如果试图访问这些数据,就可能导致访问违规。这是使用静态T L S的唯一不足之处。当使用动态T L S时,不会出现这个问题。使用动态T L S的库可以在运行期进行加载,并且可以在运行期释放,根本不会产生任何问题。