线程局部存储(thread_local)

C++11中的几种存储

序号类型存储器及链接方式备注

1

auto

自动存储期

该关键字用于两种情况:
1. 声明变量时: 根据初始化表达式自动推断变量类型。
2. 声明函数作为函数返回值的占位符。

2

static

静态或线程存储期,同时提示是内部链接

static变量只初始化一次,除此之外它还有可见性的属性:
1. static修饰函数内的“局部”变量时,表明它不需要在进入或离开函数时创建或销毁。且仅在函数内可见。
2. static修饰全局变量时,表明该变量仅在当前(声明它的)文件内可见。
3. 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的。

  1. 命名空间下的全局变量
  2. 类的static成员变量
  3. 本地局部变量
  4. 文件静态变量

 

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"段中。

 

posted @ 2021-07-18 21:06  gd_沐辰  阅读(1572)  评论(0编辑  收藏  举报