发信人: skyice()
整理人: skyice(2000-06-22 00:30:17), 站内信件
|
线程局部存储
线程局部存储提供了把数据连接到线程的能力。Win32 支持两种类型的
线程局部存储:动态的和静态的。尽管动态线程局部存储使用时最麻烦,但
它提供了最大的灵活性。(即,可以用在动态载入的动态链接库,即用 Lo-
adLobrary()载入的 DLL。)静态线程局部存储使用比较容易,实际上,容
易得于几乎无法把它从常规全局变量中区别出来。不幸的是,何时使用静态
线程局部存储有一个限制。在动态载入的DLL中,操作系统将禁用这个特性。
正象前面所说,线程是 Win32 程序的调度单元。当 Windows 98 或
Windows NT上的 Win32进程开始运行时,Windows就启动一个线程。(在
其他Win32s中不支持线程。)之后,得到新线程的唯一方法是调用 Win32
函数 CreateThread()。C 运行库提供了一个包装函数__beginthread(),
它调用了 CreateTHread(),这个函数把 C 运行库设置为认可线程的。在
需要创建新线程时,MFC 调用这个函数(事实上,它调用 __beginthrea-
dex(),这是 __begininthread() 的一个轻微变形。)
为跟踪特定于线程的数据,诸如 Windows 98 和 Windows NT 这样的
操作系统分配内存以容纳线程实例数据。在 Windows NT 中,这个内存区
域叫做线程环境块(Thread Environment Block,TEB)。在Windows 98
中,根据 Andrew Schulman 的报告,叫做线程控制块(Thread Control
Block,THCB)。不管名称是什么,这些内存块存放的都是 Windows 为单
个线程维护的操作系统实例数据。作为线程实例数据存放的数据类型包括:
线程未在运行时处理器寄存器的状态,一个指向线程的栈的指针,还有一个
指向存储在栈中的异常数据的指针。
动态线程局部存储,和窗口额外字节一样,不过就是存储在一个操作系
统的数据结构中的应用程序数据。在 Windows NT 或 Windows 98 下,当
创建一个线程时,就会分配一个 TEB 或 THCB 结构。除了每个操作系统为
自己使用而分配的内存,还另外设置了一个 DWORD(unsigned long)数组
供应用程序使用。应用程序代码(无论是 EXE 代码还是 DLL 代码)的任何
部分都可以通过调用API 预留一个数组项,给定进程中的所有线程都可以使
用 API。例如,如果一个进程有三个线程,用于预留动态线程局部存储的成
功调用提供对三个 DWORD 项的访问,每个线程一个。当给定代码块正在运
行时,它自动为它在其中运行的当前线程读取线程局部存储值。
EXE 代码或 DLL 代码会怎样使用动态线程局部存储呢?在 DWORD 中
的 4 个字节适合任何 32 位指针,例如指向动态分配数据的指针。如果程
序为后台任务创建线程(诸如打印、数据排序或数据库完整性检查等),特
定于线程的数据可以存放一个要完成的任务的队列。维护动态线程局部存储
的 Win32 API 函数在下表中描述:
动态线程局部存储
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
函数 说明
────────────────────────────────
为当前进程中的所有线程申请一个特定于应用程序
TlsAlloc() 的存储储数组元素,特定于应用程序的存储是Win-
dows 在每个线程的线程实例数据结构中留出的。
────────────────────────────────
TlsFree() 在当前进程中,把一个下标放回到线程实例数据结
构中。
────────────────────────────────
TlsGetValue() 对于当前进程,查询线程实例数据中指定元素的内
容。
────────────────────────────────
TlsSetValue() 对于当前进程,设置线程实例数据中指定元素的内
容。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
调用 TlsAlloc() 分配线程局部存储时,API函数返回线程数据数组的
一个下标。这个下标必须存放在一个共享内存区中,通常是在全局变量中。
这样就可以动态分配单线程数据以及作为线程局部存储而存储的数据指针。
第二种单线程数据类型是静态线程局部存储。它很容易使用,因为它看
起来就象全局变量。事实上,编译器和加载器一起合作,为在进程内创建的
每个线程自动分配一组新的静态线程局部存储。如果你查看一下编译器生成
的访问这个数据的汇编语言代码,就会看到,一些额外的指令为正常情况下
用来访问数据的东西另加了一个间址。静态线程局部存储的精彩之处在于它
看起来象一个全局变量,但行为象单线程数据。
静态线程局部数据的关键是一对微软 C 编译器专用的关键字:__dec-
lspec 和 thread。下面是如何定义基于单线程的整数值:
__declspec(thread) int i = 0;
所有的编译器关键字都以一个下划线(__)开头,表示不是 ANSI标准
的 C/C++ 关键字。过去,微软一度随意地把新声明符加到它的编译器中:
__pascal、__cdecl、__near、__far、__stdcall,等等。但是当这样的
声明符与成为标准 C++ 的一部分的新关键字相冲突时,就会产生问题。
为控制新声明符定义的增加,微软创建了一个更通用的万能声明符:__
declspec。这个声明符用来作为主声明符。所有新的编译器扩充(如静态线
程局部存储)都将使用对主声明符的修改符来访问。目前,我们只知道三个
另外的声明符定义:dllimport、dllexport 和 naked。必要时还将创建更
多的声明符以扩充编译器。为使静态线程局部存储的声明多少更具些可读性,
创建了一个预处理符号:
#define THREADDATA __declspec(thread)
前面的单进程整数可以改写成下面这样可读性更好一点的形式:
THREADDATA int i = 0;
单进程数据必须初始化。前面的例子把这个整型变量设置为 0。尽管这
对应用程序的程序员来说只是略有不同,但对编译器来说却是天壤之别,编
译器对待已初始化的数据和未初始化数据是截然不同的。当前编译器实现不
支持单线程的未初始化数据,但是支持已经初始化的单线程数据。有个小差
别有助于确保你获得所需的数据支持(在本章后面关于共享数据的讨论中,
你还将看到,在进程间共享的全局变量也必须初始化)。
我们曾说过,这种类型的单线程数据的主要优点是看起来就象一个常规
全局变量。一旦定义了,你就不必再费劲地用 API 调用来访问数据;你需
要它时,它自然就会出现。如果看一看编译器生成的机器代码,你将注意到
访问这种类型的单线程数据的确需要一些额外的指令。但当你需要单线程数
据时,这只是很小的代价。
这种类型的单线程数据的另一个优点是需要多少就可以分配多少。尽管
这个例子显示的是单个变量的分配,但分配数千甚至数万的字节也并非难事。
可执行文件中,基于单线程分配的变量放在它们自己的段中。所以,就象其
他代码或数据段一样,这样的数据在必要时可以达到数兆字节。
然而,对静态线程局部存储还有一个限制。它不能用于动态载入内存的
动态链接库。如果程序调用 LoadLibrary() 显式地把一个 DLL 载入内存,
这时就不能使用静态线程局部存储。这样载入内存的一个 DLL 例子是打印
机驱动程序。对于静态载人的DLL,静态线程局部存储工作得很好。这些DLL
是在可执行文件启动时载入内存的。然而除了这个限制,静态线程局部存储
提供了一种把应用程序数据连接到系统线程的非常好的方法。
对私有的、单进程内存的讨论到这里就结束了。现在我们把注意力转向
可作为进程间共享内存的所有内存类型。
-- 独人独剑独马
浪迹天涯 ...
※ 来源:.月光软件站 http://www.moon-soft.com.[FROM: 202.99.93.147]
|
|