线程本地存储 (TLS) 是一个方法,通过该方法,给定的多线程进程中的每个线程都可以分配存储线程特定数据的位置。通过 TLS API(TlsAlloc、TlsGetValue、TlsSetValue、TlsFree)方式支持动态绑定(运行时)的线程特定数据。除了现有的 API 实现,Win32 和 Visual C++ 编译器现在还支持静态绑定(加载时间)基于线程的数据。
TLS 的 API 实现
通过 Win32 API 层和编译器实现“线程本地存储”。有关详细信息,请参见 Win32 API 文档中的 TlsAlloc、TlsGetValue、TlsSetValue 和 TlsFree。
Visual C++ 编译器包括使线程本地存储操作更加自动化的关键字,而不是通过 API 层。将在下一节(TLS 的编译器实现)描述此语法。
TLS 的编译器实现
为了支持 TLS,已将新属性 thread 添加到了 C 和 C++ 语言,并由 Visual C++ 编译器支持。此属性是一个扩展存储类修饰符,如上一节中所述。使用 __declspec 关键字声明 thread 变量。例如,以下代码声明了一个整数线程局部变量,并用一个值对其进行初始化:
__declspec( thread ) int tls_i = 1;
TLS 的规则和限制
声明静态绑定线程的本地对象和变量时必须遵守下列原则:
- thread 属性只能应用于数据声明和定义。它不能用于函数声明或定义。例如,以下代码将生成一个编译器错误:
#define Thread __declspec( thread )
Thread void func(); // This will generate an error.
- 只能在具有 static 作用域的数据项上指定 thread 修饰符。包括全局数据对象(包括 static 和 extern)、本地静态对象和 C++ 类的静态数据成员。不可以用 thread 属性声明自动数据对象。以下代码将生成编译器错误:
#define Thread __declspec( thread )
void func1()
{
Thread int tls_i; // This will generate an error.
}
int func2( Thread int tls_i ) // This will generate an error.
{
return tls_i;
}
- 线程本地对象的声明和定义必须全都指定 thread 属性。例如,以下代码将生成错误:
#define Thread __declspec( thread )
extern int tls_i; // This will generate an error, since the
int Thread tls_i; // declaration and definition differ.
- thread 属性不能用作类型修饰符。例如,以下代码将生成一个编译器错误:
char __declspec( thread ) *ch; // Error
- C++ 类不能使用 thread 属性。但是,可以使用 thread 属性将 C++ 类对象实例化。例如,以下代码将生成一个编译器错误:
#define Thread __declspec( thread )
class Thread C // Error: classes cannot be declared Thread.
{
// Code
};
C CObject;
因为允许使用 thread 属性的 C++ 对象的声明,因此下面两个示例在语义上是等效的:
#define Thread __declspec( thread )
Thread class B
{
// Code
} BObject; // OK--BObject is declared thread local.
class B
{
// Code
};
Thread B BObject; // OK--BObject is declared thread local.
- 不将线程本地对象的地址视为常数,并且涉及此类地址的任何表达式都不视为常数。在标准 C 中,这种作法的效果是禁止将线程本地变量的地址用作对象或指针的初始值设定项。例如,C 编译器将以下代码标记为错误:
#define Thread __declspec( thread )
Thread int tls_i;
int *p = &tls_i; //This will generate an error in C.
但是,此限制不适用于 C++。因为 C++ 允许动态初始化所有对象,因此可以用使用线程本地变量地址的表达式初始化对象。实现此操作的方式与实现线程本地对象结构的方式相同。例如,以上显示的代码在作为 C++ 源文件编译时不会生成错误。请注意:只有在其中获取地址的线程仍然存在的情况下,线程本地变量的地址才有效。
- 标准 C 允许使用涉及引用自身的表达式初始化对象或变量,但只适用于非静态作用域的对象。虽然 C++ 通常允许使用涉及引用自身的表达式动态初始化对象,但是这种类型的初始化不允许用于线程本地对象。例如:
#define Thread __declspec( thread )
Thread int tls_i = tls_i; // Error in C and C++
int j = j; // OK in C++, error in C
Thread int tls_i = sizeof( tls_i ) // Legal in C and C++
请注意:包含正在初始化的对象的 sizeof
表达式不建立对自身的引用且在 C 和 C++ 中都是合法的。
C++ 不允许此类对线程数据的动态初始化,因为将来可能要对线程本地存储功能进行增强。
- 如果 DLL 将任何非本地数据或对象声明为 __declspec(线程),动态加载该 DLL 时会导致保护错误。使用 LoadLibrary 加载所有 DLL 后,每当代码引用非本地 __declspec(线程)数据时,将导致系统故障。由于线程的全局变量空间是在运行时分配的,因此此空间的大小是以应用程序的需求和所有静态链接的 DLL 的需求相加为基础计算出来的。使用 LoadLibrary 时,无法扩展此空间以允许放置用 __declspec(线程)声明的线程本地变量。如果 DLL 可能是用 LoadLibrary 加载的,请在 DLL 中使用 TLS API(如 TlsAlloc)来分配 TLS。