线程局部存储(thread_local)
C++11中的几种存储
序号 | 类型 | 存储器及链接方式 | 备注 |
---|---|---|---|
1 |
auto |
自动存储期 |
该关键字用于两种情况: |
2 |
static |
静态或线程存储期,同时提示是内部链接 |
static变量只初始化一次,除此之外它还有可见性的属性: |
3 |
register |
自动存储期,提示编译器将此变量置于寄存器上 |
寄存器变量。该变量存储在CPU寄存器中,而不是RAM(栈或堆)中。 |
4 |
extern |
静态或线程存储期,同时提示是外部链接 |
引用一个全局变量。当在一个文件中定义了一个全局变量时,就可以在其它文件中使用extern来声明并引用该变量。 |
5 |
mutable |
不影响存储期或链接 |
仅适用于类成员变量。以mutable修饰的成员变量可以在const成员函数中修改。 |
6 |
thread_local |
线程存储期 |
线程周期 |
thread_local的生命周期
- 线程局部存储(Thread Local Storage,TLS)是一种存储期(storage duration),对象的存储是在线程开始时分配,线程结束时回收,每个线程有该对象自己的实例。
thread_local的原理与实现
参考链接:
https://www.cnblogs.com/zhoug2020/p/6497709.html
https://www.jianshu.com/p/495ea7ce649b
哪些变量可以被声明为thread_local
- 只对声明于命名空间作用域的对象、声明于块作用域的对象及静态数据成员允许。
- thread_local作为类成员变量时必须是static的。
即
- 命名空间下的全局变量
- 类的static成员变量
- 本地局部变量
- 文件静态变量
thread_local int x; //A thread-local variable at namespace scope class X { static thread_local std::string s; //A thread-local static class data member }; static thread_local std::string X::s; //The definition of X::s is required void foo() { thread_local std::vector<int> v; //A thread-local local variable } |
thread_local修饰的变量具有如下特性
- 变量在线程创建时生成(不同编译器实现略有差异,但在线程内变量第一次使用前必然已构造完毕)。
- 线程结束时被销毁(析构,利用析构特性,thread_local变量可以感知线程销毁事件)。
- 每个线程都拥有其自己的变量副本。
- thread_local可以和static或extern联合使用,这将会影响变量的链接属性。
测试Demo
#include <iostream> #include <thread> thread_local int gCount = 10; class Test { public: Test() { }; void test(const std::string &name) { thread_local int count = 1; ++count; _count += 100; gCount += 10; std::cout << name << "--count: " << count << std::endl; std::cout << name << "--gCount: " << gCount << std::endl; std::cout << name << "--_count: " << _count << std::endl; } private: static thread_local int _count; }; thread_local int Test::_count = 100; void func(const std::string &name) { Test a1; a1.test(name); a1.test(name); Test a2; a2.test(name); a2.test(name); } int main() { std::thread t1(func, "t1"); t1.join(); std::cout << std::endl; std::thread t2(func, "t2"); t2.join(); return 0; } |
结果:
说明
对于Windows系统来说,全局变量或静态变量会被放到".data"或".bss"段中,但当使用__declspec(thread)定义一个线程私有变量的时候,编译器会把这些变量放到PE文件的".tls"段中。当系统启动一个新的线程时,它会从进程的堆中分配一块足够大小的空间,然后把".tls"段中的内容复制到这块空间中,于是每个线程都有自己独立的一个".tls"副本。所以对于用__declspec(thread)定义的同一个变量,它们在不同线程中的地址都是不一样的。对于一个TLS变量来说,它有可能是一个C++的全局对象,那么每个线程在启动时不仅仅是复制".tls"的内容那么简单,还需要把这些TLS对象初始化,必须逐个地调用它们的全局构造函数,而且当线程退出时,还要逐个地将它们析构,正如普通的全局对象在进程启动和退出时都要构造、析构一样。Windows PE文件的结构中有个叫数据目录的结构。它总共有16个元素,其中有一元素下标为IMAGE_DIRECT_ENTRY_TLS,这个元素中保存的地址和长度就是TLS表(IMAGE_TLS_DIRECTORY结构)的地址和长度。TLS表中保存了所有TLS变量的构造函数和析构函数的地址,Windows系统就是根据TLS表中的内容,在每次线程启动或退出时对TLS变量进行构造和析构。TLS表本身往往位于PE文件的".rdata"段中。