6.4 线程局部存储
类别:所有人
线程局部存储(TLS,thread local storage)是一个已有的概念。简单地说,所谓线程局部存储变量,就是拥有线程生命期及线程可见性的变量。
线程局部存储实际上是由单线程程序中的全局/静态变量被应用到多线程程序中被线程共享而来。我们可以简单地回顾一下所谓的线程模型。通常情况下,线程会拥有自己的栈空间,但是堆空间、静态数据区(如果从可执行文件的角度来看,静态数据区对应的是可执行文件的data、bss段的数据,而从C/C++语言层面而言,则对应的是全局/静态变量)则是共享的。这样一来,全局、静态变量在这种多线程模型下就总是在线程间共享的。
全局、静态变量的共享虽然会带来一些好处,尤其对一些资源性的变量(比如文件句柄)来说也是应该的,不过并不是所有的全局、静态变量都适合在多线程的情况下共享。我们可以看看代码清单6-27所示的例子。
代码清单6-27
include <pthread.h>
include <iostream>
using namespace std;
int errorCode=0;
voidMaySetErr(voidinput){
if((int)input==1)
errorCode=1;
else if((int)input==2)
errorCode=-1;
else
errorCode=0;
}
int main(){
int input_a=1;
int input_b=2;
pthread_t thread1,thread2;
pthread_create(&thread1,NULL,&MaySetErr,&input_a);
pthread_create(&thread2,NULL,&MaySetErr,&input_b);
pthread_join(thread2,NULL);
pthread_join(thread1,NULL);
cout<<errorCode<<endl;
}
//编译选项:g++6-4-1.cpp-lpthread
在代码清单6-27中,函数MaySetErr函数可能会根据输入值input设置全局的错误码errorCode。当用两个线程运行该函数的时候,最终获得的errorCode的值将是不确定的,或者说,将由系统如何调度两个线程而决定。实际上,本例中的errorCode即是POSIX标准中的错误码全局变量errno在多线程情况下遭遇的问题的一个简化。一旦errno在线程间共享,则一些程序中运行的错误将会被隐藏不报。而解决的办法就是为每个线程指派一个全局的errno,即TLS化的errno。
各个编译器公司都有自己的TLS标准。我们在g++/clang++/xlc++中可以看到如下的语法:
__thread int errCode;
即在全局或者静态变量的声明中加上关键字__thread,即可将变量声明为TLS变量。每个线程将拥有独立的errCode的拷贝,一个线程中对errCode的读写并不会影响另外一个线程中的errCode的数据。
C++11对TLS标准做出了一些统一的规定。与__thread修饰符类似,声明一个TLS变量的语法很简单,即通过thread_local修饰符声明变量即可。
int thread_local errCode;
一旦声明一个变量为thread_local,其值将在线程开始时被初始化,而在线程结束时,该值也将不再有效。对于thread_local变量地址取值(&),也只可以获得当前线程中的TLS变量的地址值。
虽然TLS变量的声明很简单,使用也很直观,不过实际上TLS的实现需要涉及编译器、链接器、加载器甚至是操作系统的相互配合。在TLS中一个常被讨论的问题就是TLS变量的静态/动态分配的问题,即TLS变量的内存究竟是在程序一开始就被分配还是在线程开始运行时被分配。通常情况下,前者比后者更易于实现。C++11标准允许平台/编译器自行选择采用静态分配或动态分配,或者两者都支持。还有一点值得注意的是,C++11对TLS只是做了语法上的统一,而对其实现并没有做任何性能上的规定。这可能导致thread_local声明的变量在不同平台或者不同的TLS实现上出现不同的性能(通常TLS变量的读写性能不会高于普通的全局/静态变量)。如果读者想得到最佳的平台上的TLS变量的运行性能的话,最好还是阅读代码运行平台的相关文档。