线程本地存储 (TLS) 是一种方法,给定的多线程进程中的每个线程可以使用这种方法分配用以存储线程特定的数据的位置。 通过 TLS API (TlsAlloc) 支持动态绑定运行时线程特定数据。 Win32 和 Microsoft C++ 编译器现在除了支持现有的 API 实现外,还支持静态绑定负载时线程数据。
进程的所有线程共享其虚拟地址空间。 函数的局部变量对运行函数的每个线程都是唯一的。 但是,静态变量和全局变量由进程中的所有线程共享。 使用 线程本地存储 (TLS) ,可以为进程可以使用全局索引访问的每个线程提供唯一数据。 一个线程分配索引,其他线程可以使用该索引来检索与索引关联的唯一数据。
常量TLS_MINIMUM_AVAILABLE定义每个进程中可用的 TLS 索引的最小数目。 对于所有系统,此最小值保证至少为 64。 每个进程的最大索引数为 1088。
创建线程时,系统会为 TLS 分配一个 LPVOID 值数组,这些值初始化为 NULL。 索引必须由其中一个线程分配,然后才能使用索引。 每个线程将其 TLS 索引的数据存储在数组的 TLS 槽 中。 如果与索引关联的数据适合 LPVOID 值,则可以将数据直接存储在 TLS 槽中。 但是,如果以这种方式使用大量索引,最好分配单独的存储、合并数据,并最大程度地减少使用的 TLS 槽数。
下图演示了 TLS 的工作原理:
进程有两个线程:线程 1 和线程 2。 它分配两个用于 TLS 的索引:gdwTlsIndex1 和 gdwTlsIndex2。 每个线程为存储数据的每个索引分配两个内存块,并将指向这些内存块的指针存储在相应的 TLS 槽中。 为了访问与索引关联的数据,线程从 TLS 槽中检索指向内存块的指针,并将其存储在 lpvData 本地变量中。
最好在动态链接库中 (DLL) 中使用 TLS。
TLS 的编译器实现
C++11:建议使用 thread_local 存储类说明符指定对象和类成员的线程本地存储。
MSVC 还提供特定于 Microsoft 的特性(线程)作为扩展的存储类修饰符。 可以使用 __declspec 关键字声明 thread 变量。 例如,以下代码声明了一个整数线程局部变量,并用一个值对其进行初始化:
__declspec( thread ) int tls_i = 1;
规则和限制
声明静态绑定的线程本地对象和变量时,必须遵守以下准则: 这些准则同时适用于 thread 和 thread_local:
-
thread特性仅可适用于类以及数据声明和定义。 它不能用于函数声明或定义。 例如,下面的代码生成一个编译器错误:
__declspec( thread )void func(); // This will generate an error.
-
thread
修饰符只能在具有static
盘区的数据项上指定。 其中包括全局数据对象(static
和extern
),本地静态对象以及 C ++ 类的静态数据成员。 不能使用thread
特性声明自动数据对象。 以下代码生成编译器错误:void func1() { __declspec( thread )int tls_i; // This will generate an error. } int func2(__declspec( 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 __declspec( thread )tls_i; // declaration and definition differ.
-
thread
特性不能用作类型修饰符。 例如,下面的代码生成一个编译器错误:char __declspec( thread ) *ch; // Error
-
由于允许执行使用
thread
特性的 C++ 对象的声明,因此以下两个示例在语义上是等效的:__declspec( thread ) class B { // Code } BObject; // OK--BObject is declared thread local. class B { // Code }; __declspec( thread ) B BObject; // OK--BObject is declared thread local.
-
线程本地对象的地址不视为常数,并且涉及此类地址的任何表达式不会被视为常数表达式。 在标准 C 中,此操作的效果是禁止将线程本地变量的地址用作对象或指针的初始值设定项。 例如,C 编译器会将以下代码标记为错误:
__declspec( thread ) int tls_i; int *p = &tls_i; //This will generate an error in C.
此限制不适用于 C++。 由于 C++ 允许所有对象的动态初始化,可以通过利用使用线程本地变量的地址的表达式将对象初始化。 就像完成线程本地对象的构造一样完成此操作。 例如,当编译成 C++ 源文件时,前面所示的代码不生成错误。 仅当从中获取地址的线程仍然存在时,线程本地变量的地址才有效。
-
标准 C 允许使用涉及引用自身的表达式进行对象或变量的初始化,但只适用于非静态范围的对象。 虽然 C++ 通常允许使用涉及引用自身的表达式进行此类的对象动态初始化,但是不允许对线程本地对象进行这种初始化。 例如:
__declspec( thread )int tls_i = tls_i; // Error in C and C++ int j = j; // OK in C++, error in C __declspec( thread )int tls_i = sizeof( tls_i ) // Legal in C and C++
包含正在初始化的对象的
sizeof
表达式不表示对自身的引用,并在 C 和 C++ 中都启用。因为将来可能要对线程本地存储功能进行增强,C++ 不允许对线程数据进行此类的动态初始化。 -
在Windows Vista 之前的 Windows 操作系统上,__declspec( thread ) 具有一些限制。 如果 DLL 将任何数据或对象声明为 __declspec( thread ),可能会导致保护错误(如果动态加载)。 使用 LoadLibrary 加载 DLL 后,只要代码引用 __declspec( thread ) 数据,就会导致系统故障。 由于线程的全局变量空间在运行时进行分配,此空间的大小基于对应用程序的要求加静态链接的所有 DLL 的要求的计算。 使用 LoadLibrary 时,不能扩展此空间以允许使用 __declspec( thread ) 声明的线程本地变量。 如果该 DLL 可能使用 LoadLibrary 进行了加载,则使用 DLL 中的 TLS API(如 TlsAlloc)来分配 TLS。