深入理解c++11thread_local

thread_local

thread_local 是c++的关键字,可以用来修饰全局变量、类的静态成员变量以及函数的局部变量,表示每一个线程都拥有此变量的一个实例拷贝,相当于每个线程都拥有一个变量。例如:

thread_local int global_var; // 全局的thread_local变量

void Foo()
{
    
    
    thread_local int local_var; // 函数内的局部thread_local变量
}

class Wigdet {
    
    
private:
    static thread_local int static_var; // 类内的静态thread_local变量
};
thread_local int Wigdet::static_var; // 像普通类内静态变量那样

thread_local的生命周期

为了探寻thread_loacl的生命周期,我们可以搞个自定义类型,暂且就定位widget吧,并在widget的构造函数以及析构函数中打印相应的操作,就像这样。

#include<iostream>
#include<string>
#include<thread>
class Wigdet {
    
    
public:
    explicit Wigdet(const std::string &strName); // 构造
    Wigdet(const Wigdet& othre); // copy构造
    Wigdet& operator=(const Wigdet& othre) = delete; // assign 构造
    Wigdet(Wigdet&& othree) = delete;
    Wigdet& operator=(Wigdet&&) = delete;
    ~Wigdet ();
    friend void PrintMsg(const Wigdet& widget, const std::string& msg);
    friend void PrintNum(const Wigdet& widget);
    int Add(); // 调用一次+1
    int GetNum(); // 返回m_num
private:
    std::string m_strThreadName;
    int m_num; // 试着在不同的线程查看不同的结果
};

Wigdet::Wigdet (const std::string& strThreadName) 
    : m_strThreadName(strThreadName),
    m_num(0)
{
    
    
    PrintMsg(*this,"构造函数被调用。");
}

Wigdet::~Wigdet ()
{
    
    
    PrintMsg(*this, "析构函数被调用。");
}

int Wigdet::Add()
{
    
    
    return ++m_num;
}

int Wigdet::GetNum()
{
    
    
    return m_num;
}

Wigdet::Wigdet(const Wigdet& othre)
    : m_strThreadName(othre.m_strThreadName),
    m_num(othre.m_num)
{
    
    
    PrintMsg(*this, "copy 构造函数被调用。");
}

// 打印消息的友元函数
void PrintMsg(const Wigdet& widget, const std::string& msg)
{
    
    
    std::cout << "线程id: " 
        << std::this_thread::get_id() << "\t" 
        << widget.m_strThreadName << msg << std::endl;
}

void PrintNum(const Wigdet& widget)
{
    
    
    // 由于std::cout 并非原子操作,会产生竞争
    // 为了方便,使一个线程结束了再启动另外一个线程,便于分析
    std::cout << "线程id: "
        << std::this_thread::get_id() << "\t"
        << widget.m_strThreadName << " 当前num值:" << widget.m_num << std::endl;
}

全局thread_local

使用简单的代码来测试一下全局htread_local, 多线程是运行方式是抢占式的运行,他们都很没有礼貌,每个线程都像十几天没吃饭的饿汉那样,只要有吃的就拼了命的抢,直到吃饱为止,为了杜绝这种不礼貌的行为。我搞了个大门,一次只能进去一个线程,等他慢慢吃,吃饱了才放另外一个进去,这样分析问题就简单了许多。但去掉这个大门,他们真的像饿狗扑食那样,毫无章法,只是一个劲的抢占cpu。测试代码这样就差不多了,暂时不用太多的并发控制。

// 全局thread_local 测试
thread_local Wigdet global_wigdet(std::string("global_var"));

void Foo()
{
    
    
    PrintNum(global_wigdet);
    global_wigdet.Add();
    PrintNum(global_wigdet);
}

int main()
{
    
    
    std::cout << "主线程ID :" << std::this_thread::get_id() << std::endl;
    Foo(); // 主线程调用;
     // 使用超长延时使代码按照书写方式运行,便于分析。
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::thread A(Foo); // 线程A调用
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::thread B(Foo); // 线程B调用
    A.join();
    B.join();
}

运行结果如下:
线程id: 3732 global_var构造函数被调用。
主线程ID :3732
线程id: 3732 global_var 当前num值:0
线程id: 3732 global_var 当前num值:1
线程id: 16240 global_var构造函数被调用。
线程id: 16240 global_var 当前num值:0
线程id: 16240 global_var 当前num值:1
线程id: 16240 global_var析构函数被调用。
线程id: 2852 global_var构造函数被调用。
线程id: 2852 global_var 当前num值:0
线程id: 2852 global_var 当前num值:1
线程id: 2852 global_var析构函数被调用。
线程id: 3732 global_var析构函数被调用。

  1. 从输出来分析就简单多了,首先global_var在主线程中初始化,为什么是主线程呢,main()函数中第一句不就打出来吗,哈哈。初始化早于main()的第一句语句运行时间,证明初始化和全局变量初始化差不多,都是在程序刚刚启动就执行初始化,当我们从主函数调用Foo(),得到初始num为0,执行Widget::Add()方法,num变为1了。一切都按照预想的进行。当我再开启一个线程A,global_var又被重新构造了一份,并且将num初始化为1,等到A线程退出时候,线程A中的global_var析构函数被调用,又开启一个线程B,从打印结果来看,也是重新构造了一份,结局也和A相同,随着线程B的消亡,B中的global_var也不得不调用析构函数释放自己。然后主线程退出了,主线程拥有的global_var也析构了。

  2. 总结一下上面的过程可知:全局的thead_local会首先在主线程中构造,直到程序退出才进行析构,并且每个使用过global_var的线程都拥有一份全新的实例拷贝。每个线程管理自己的global_var,直到线程自己退出才销毁global_var.

局部thread_local

使用同样的方法,在测试一下局部的thread_local,我们在测试函数TestLocal()中定义局部的thread_local,并使用FooLocal() 函数来调用两次,为什么是调用两次呢,主要是测试一下局部的thread_local是否会有单线程中的static静态局部变量一样的行为。

void TestLocal(const std::string& name)
{
    
    
    thread_local Wigdet local_var(name);
    PrintNum(local_var);
    local_var.Add();
    PrintNum(local_var);
}

void FooLocal(const std::string& name)
{
    
    
    TestLocal(name);
    TestLocal(name);
}
int main() // 测试局部变量
{
    
    
    std::cout << "主线程ID :" << std::this_thread::get_id() << std::endl;
    FooLocal("主线程 "); // 主线程调用;
     // 使用超长延时使代码按照书写方式运行,便于分析。
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::thread A(FooLocal,"A "); // 线程A调用
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::thread B(FooLocal, "B "); // 线程B调用
    A.join();
    B.join();
}

程序输出结果:
主线程ID :12452
线程id: 12452 主线程 构造函数被调用。
线程id: 12452 主线程 当前num值:0
线程id: 12452 主线程 当前num值:1
线程id: 12452 主线程 当前num值:1
线程id: 12452 主线程 当前num值:2
线程id: 3204 A 构造函数被调用。
线程id: 3204 A 当前num值:0
线程id: 3204 A 当前num值:1
线程id: 3204 A 当前num值:1
线程id: 3204 A 当前num值:2
线程id: 3204 A 析构函数被调用。
线程id: 6524 B 构造函数被调用。
线程id: 6524 B 当前num值:0
线程id: 6524 B 当前num值:1
线程id: 6524 B 当前num值:1
线程id: 6524 B 当前num值:2
线程id: 6524 B 析构函数被调用。
线程id: 12452 主线程 析构函数被调用。

从输出分析来看,局部的thread_local和局部static变量很相似。主线程中static局部变量是在函数第一次运行时候进行初始化,从第二次以后就跳过初始化,直接使用该变量。局部的thread_local就很像他,只不过局部thread_local的生命周期是在线程启动时候进行初始化,到线程退出时候进行析构销毁。总之,用理解局部static的方法理解局部thread_local准没错的。

类内静态成员thread_local

class Test {
    
    
public:
    Test(const std::string &name)
        :m_name(name)
    {
    
     
        std::cout << std::this_thread::get_id() 
            << name << "构造" <<std::endl;
    }
    ~Test()
    {
    
    
        std::cout << std::this_thread::get_id() 
            << m_name << "销毁" <<std::endl;
    }
    void Add() {
    
    
        m_wigdet.Add();
        PrintNum(m_wigdet);
    }
private:
  static thread_local Wigdet m_wigdet;
  std::string m_name;
};

thread_local Wigdet Test::m_wigdet(std::string("static"));

void FooStatic(const std::string& name)
{
    
    
    // 多构造几个测试类,就可以看到猫腻
    Test test(name);
    test.Add();
    Test test_b(name);
    test_b.Add();
}

int main() // 测试类内的static 局部变量。
{
    
    
    std::cout << "主线程ID :" << std::this_thread::get_id() << std::endl;
    FooStatic("主线程 "); // 主线程调用;
    // 使用超长延时使代码按照书写方式运行,便于分析。
    std::this_thread::sleep_for(std::chrono::seconds(1)); 
    std::thread A(FooStatic,"A "); // 线程A调用
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::thread B(FooStatic, "B "); // 线程B调用
    A.join();
    B.join();
}

看一下输出结果:
线程id: 15176 static构造函数被调用。
主线程ID :15176
15176主线程 构造
线程id: 15176 static 当前num值:1
15176主线程 构造
线程id: 15176 static 当前num值:2
15176主线程 销毁
15176主线程 销毁
线程id: 14372 static构造函数被调用。
14372A 构造
线程id: 14372 static 当前num值:1
14372A 构造
线程id: 14372 static 当前num值:2
14372A 销毁
14372A 销毁
线程id: 14372 static析构函数被调用。
线程id: 4116 static构造函数被调用。
4116B 构造
线程id: 4116 static 当前num值:1
4116B 构造
线程id: 4116 static 当前num值:2
4116B 销毁
4116B 销毁
线程id: 4116 static析构函数被调用。
线程id: 15176 static析构函数被调用。

通过输出结果分析得出,类内静态变量也是会在每个线程中拷贝一份全新的实例,只要有线程开辟,那么就会构造一个新的thread_local,但是呢每个线程只有一个静态的 thread_local变量,不管构造多少个测试类,thread_local在一个线程中只有一份。这么想想好像还可以用这种方法统计每个线程中的某个类实例了多少份。当然,在线程退出的时候,每一份拷贝的实例也得析构销毁。就像单线程中的类内static 一样,程序启动的时候构造,程序退出的时候析构。而thead_local仅仅是线程开始的时候构造,线程退出的时候进行析构。另外,对于以上所有情况在,thead_local在构造的时候如果抛出一场,那么std::terminate()会调用,程序被终结。如果析构时候抛出异常,同样std::terminate()会调用。当线程调用 std::exit() 或从main()函数返回(等价于调用 std::exit() 作为main()的“返回值”)
时, thead_local变量也会为了这个线程进行销毁。 应用退出时还有线程在运行, 对于这些线程来说,thead_local变量的析构函数就没有被调用。

总结

遇到不知道的概念,应该多写写例子,特别是观察一下何时构造,何时析构,何时使用复制构造等等,这样在大脑中有一个清晰的概念,写代码时候就不易犯错误了,今个也差不多了,哎呀,怎么就两点多了,睡了睡了,明天还得加班呢。

猜你喜欢

转载自blog.csdn.net/qq_33944628/article/details/117934790
今日推荐