线程局部存储
对于一个存在多个线程的进程来说有时候我们需要有一份数据是每个线程都拥有一份的也就是说每个线程自己操作自己的这份数据这有点类似 C 类的实例属性每个实例对象操作的都是自己的属性。我们把这样的数据称之为线程局部存储Thread Local StorageTLS对应的存储区域叫做线程局部存储区。Windows 的线程局部存储Windows 系统将线程局部存储区分成TLS_MINIMUM_AVAILABLE个块每一块通过一个索引值对外提供访问。TLS_MINIMUM_AVAILABLE 的默认是 64。在 winnt.h 中定义#define TLS_MINIMUM_AVAILABLE 64Windows 中使用函数TlsAlloc获得一个线程局部存储块的索引DWORD TlsAlloc();如果这个函数调用失败返回值是TLS_OUT_OF_INDEXES0xFFFFFFFF如果函数调用成功得到一个索引接下来就可以利用这个索引去往这个内存块中存储数据或者从这个内存块中得到数据分别使用如下两个 API 函数LPVOID TlsGetValue(DWORD dwTlsIndex); BOOL TlsSetValue(DWORD dwTlsIndex, LPVOID lpTlsValue);当你不再需要这个存储区域时你应该释放它释放调用函数BOOL TlsFree(DWORD dwTlsIndex);当然使用线程局部存储除了使用上面介绍的 API 函数外Microsoft VC 编译器还提供了如下方法定义一个线程局部变量__declspec(thread) int g_mydata 1;我们看一个具体例子#include Windows.h #include iostream __declspec(thread) int g_mydata 1; DWORD __stdcall WorkerThreadProc1(LPVOID lpThreadParameter) { while (true) { g_mydata; //std::cout g_mydata g_mydata , ThreadID GetCurrentThreadId() std::endl; Sleep(1000); } return 0; } DWORD __stdcall WorkerThreadProc2(LPVOID lpThreadParameter) { while (true) { std::cout g_mydata g_mydata , ThreadID GetCurrentThreadId() std::endl; Sleep(1000); } return 0; } int main() { HANDLE hWorkerThreads[2]; hWorkerThreads[0] CreateThread(NULL, 0, WorkerThreadProc1, NULL, 0, NULL); hWorkerThreads[1] CreateThread(NULL, 0, WorkerThreadProc2, NULL, 0, NULL); CloseHandle(hWorkerThreads[0]); CloseHandle(hWorkerThreads[1]); while (true) { Sleep(1000); } return 0; }上述代码中全局变量g_mydata是一个线程局部变量因此该进程中每一个线程都会拥有这样一个变量副本由于是不同的副本WorkerThreadProc1中将这个变量不断递增对WorkerThreadProc2的g_mydata不会造成任何影响因此其值始终是1。程序执行结果如下需要说明的是在 Windows 系统中被声明成线程局部变量的对象在编译器生成可执行文件时会在最终的 PE 文件中专门生成一个叫 tls 的节这个节用于存放这些线程局部变量。Linux 的线程局部存储Linux 系统上的 NTPL 提供了一套函数接口来实现线程局部存储的功能int pthread_key_create(pthread_key_t* key, void (*destructor)(void*)); int pthread_key_delete(pthread_key_t key); int pthread_setspecific(pthread_key_t key, const void* value); void* pthread_getspecific(pthread_key_t key);pthread_key_create函数调用成功会返回 0 值调用失败返回非 0 值函数调用成功会为线程局部存储创建一个新键用户通过参数key去设置调用pthread_setspecific和获取pthread_getspecific数据因为进程中的所有线程都可以使用返回的键所以参数key应该指向一个全局变量。参数destructor是一个自定义函数指针其签名是void* destructor(void* value) { /*多是为了释放value指针指向的资源*/ }线程终止时如果 key 关联的值不是 NULL那么 NTPL 会自动执行定义的 destructor 函数如果无须 解构可以将 destructor 设置为 NULL。我们来看一个具体例子#include pthread.h #include stdio.h #include stdlib.h //线程局部存储key pthread_key_t thread_log_key; void write_to_thread_log(const char* message) { if (message NULL) return; FILE* logfile (FILE*)pthread_getspecific(thread_log_key); fprintf(logfile, %s\n, message); fflush(logfile); } void close_thread_log(void* logfile) { char logfilename[128]; sprintf(logfilename, close logfile: thread%ld.log\n, (unsigned long)pthread_self()); printf(logfilename); fclose((FILE *)logfile); } void* thread_function(void* args) { char logfilename[128]; sprintf(logfilename, thread%ld.log, (unsigned long)pthread_self()); FILE* logfile fopen(logfilename, w); if (logfile ! NULL) { pthread_setspecific(thread_log_key, logfile); write_to_thread_log(Thread starting...); } return NULL; } int main() { pthread_t threadIDs[5]; pthread_key_create(thread_log_key, close_thread_log); for(int i 0; i 5; i) { pthread_create(threadIDs[i], NULL, thread_function, NULL); } for(int i 0; i 5; i) { pthread_join(threadIDs[i], NULL); } return 0; }上述程序一共创建 5 个线程每个线程都会自己生成一个日志文件每个线程将自己的日志写入自己的文件中当线程执行结束时会关闭打开的日志文件句柄。程序运行结果如下生成的 5 个日志文件中其内容都写入了一行“Thread starting...”。上面的程序首先调用 pthread_key_create 函数来申请一个槽位。在NPTL实现下pthread_key_t 是无符 号整型pthread_key_create 调用成功时会将一个小于1024 的值填入第一个入参指向的 pthread_key_t 类型 的变量中。之所以小于1024是因为 NPTL 实现一共提供了1024个槽位。 如图8-1所示记录槽位分配情况的数据结构 pthread_keys 是进程唯一的pthread_keys 结构示意图如下和 Windows 一样 Linux gcc 编译器也提供了一个关键字__thread去简化定义线程局部变量。例如__thread int val 0;我们再来看一个示例#include pthread.h #include iostream #include unistd.h //线程局部存储key __thread int g_mydata 99; void* thread_function1(void* args) { while (true) { g_mydata ; } return NULL; } void* thread_function2(void* args) { while (true) { std::cout g_mydata g_mydata , ThreadID: pthread_self() std::endl; sleep(1); } return NULL; } int main() { pthread_t threadIDs[2]; pthread_create(threadIDs[0], NULL, thread_function1, NULL); pthread_create(threadIDs[1], NULL, thread_function2, NULL); for(int i 0; i 2; i) { pthread_join(threadIDs[i], NULL); } return 0; }由于thread_function1修改的是自己的g_mydata因此thread_function2输出g_mydata的值始终是99。[rootlocalhost testmultithread]# g -g -o linuxtls2 linuxtls2.cpp -lpthread [rootlocalhost testmultithread]# ./linuxtls2 g_mydata 99, ThreadID: 140243186276096 g_mydata 99, ThreadID: 140243186276096 g_mydata 99, ThreadID: 140243186276096 g_mydata 99, ThreadID: 140243186276096 g_mydata 99, ThreadID: 140243186276096 g_mydata 99, ThreadID: 140243186276096 g_mydata 99, ThreadID: 140243186276096 g_mydata 99, ThreadID: 140243186276096 g_mydata 99, ThreadID: 140243186276096 g_mydata 99, ThreadID: 140243186276096 g_mydata 99, ThreadID: 140243186276096 ...更多输出结果省略...C 11 的 thread_local 关键字C 11 标准提供了一个新的关键字thread_local来定义一个线程变量。使用方法如下thread_local int g_mydata 1;有了这个关键字使用线程局部存储的代码同时在 Windows 和 Linux 运行了。示例如下#include thread #include chrono #include iostream thread_local int g_mydata 1; void thread_func1() { while (true) { g_mydata; } } void thread_func2() { while (true) { std::cout g_mydata g_mydata , ThreadID std::this_thread::get_id() std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); } } int main() { std::thread t1(thread_func1); std::thread t2(thread_func2); t1.join(); t2.join(); return 0; }需要注意的是如果读者是在 Windows 平台下虽然thread_local关键字在 C 11 标准中引入但是 Visual Studio 2013 支持 C 11 语法的最低的一个版本编译器却并不支持这个关键字建议在 Visual Studio 2015 及以上版本中测试上述代码。最后关于线程局部存储变量我还再强调两点对于线程变量每个线程都会有该变量的一个拷贝并行不悖互不干扰。该局部变量一直都在直到线程退出为止。系统的线程局部存储区域内存空间并不大所以尽量不要利用这个空间存储大的数据块如果不得不使用大的数据块可以将大的数据块存储在堆内存中再将该堆内存的地址指针存储在线程局部存储区域。