C/C++编程:thread_local 用法

thread_local是c++11为线程安全引进的变量声明符

thread_local简介

thread_local是一个存储器指定符

所谓存储器指定符,其作用类似命名空间,指定了变量名的存储期以及链接方式。同类型的关键字还有

  • auto:自动存储期(C++11 前)。C++11 起,auto 不再是存储类说明符;它被用于指示类型推导。
  • register:自动存储期,指示编译器将此变量置于寄存器中。此关键词已于 C++17 被弃用。
  • static:静态或者线程存储期,同时指示是内部链接
  • extern:静态或线程存储期,同时提示是外部链接;
  • thread_local:线程存储期;
  • mutable:不影响存储期或链接

对于thread_local,官方解释是:

thread_local 只对声明于命名空间作用域的对象、声明于块作用域的对象以及静态数据成员允许。它指示对象拥有线程存储期。它能与 static 或 extern 结合,以分别指定内部或外部链接(除了静态数据成员始终拥有外部链接),但附加的 static 不影响存储期。

线程存储期:对象的存储在线程开始时分配,而在线程结束时解分配。每个线程拥有其自身的对象实例。唯有声明为 thread_local 的对象拥有此存储期。 thread_local 能与 static 或 extern 一同出现,以调整链接。

实践

全局变量

#include <iostream>
#include <mutex>
#include <thread>
std::mutex cout_mutex;    //方便多线程打印, 加锁指示为了方便多线程打印

thread_local int x = 1;

void thread_func(const std::string&thread_name){
    for(int i = 0; i < 50; i++){
        x++;
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "thread[" << thread_name << "]: x = " << x << std::endl;
    }
}

int main() {
    std::thread t1(thread_func, "t1");
    std::thread t2(thread_func, "t2");
    t1.join();
    t2.join();
    return 0;
}

输出

在这里插入图片描述

总结:线程们会相互抢锁,但是每个线程都会在线程初始化时拷贝一个自己的x副本,互不影响
在这里插入图片描述

局部变量

#include <iostream>
#include <mutex>
#include <thread>
std::mutex cout_mutex;    //方便多线程打印

void thread_func(const std::string& thread_name) {
    for (int i = 0; i < 3; ++i) {
        thread_local  int x = 1;  //只在每个线程创建时初始化一次
        x++;
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "thread[" << thread_name << "]: x = " << x << std::endl;
    }
}

int main() {
    std::thread t1(thread_func, "t1");
    std::thread t2(thread_func, "t2");
    t1.join();
    t2.join();
    return 0;
}

输出: 只在每个线程创建时初始化一次

thread[t1]: x = 2
thread[t1]: x = 3
thread[t1]: x = 4
thread[t2]: x = 2
thread[t2]: x = 3
thread[t2]: x = 4

如果不加thread_local,相当于局部变量

thread[t2]: x = 2
thread[t2]: x = 2
thread[t2]: x = 2
thread[t1]: x = 2
thread[t1]: x = 2
thread[t1]: x = 2

如果改成static int x = 1;,每次输出的结果都是不相同的,是线程不安全的

thread[t2]: x = 3
thread[t2]: x = 4
thread[t2]: x = 5
thread[t1]: x = 5
thread[t1]: x = 6
thread[t1]: x = 7

这里还有一个要注意的地方,就是** thread_local 虽然改变了变量的存储周期,但是并没有改变变量的使用周期或者说作用域**,比如上述的局部变量,其使用范围不能超过 for 循环外部,否则编译出错。(static也没有改变作用域,只是改变存储周期)

void thread_func(const std::string& thread_name) {
    for (int i = 0; i < 3; ++i) {
        thread_local int x = 1; // static int x = 1;
        x++;
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "thread[" << thread_name << "]: x = " << x << std::endl;
    }
    x++;    //编译会出错:error: ‘x’ was not declared in this scope
    return;
}

类对象

#include <iostream>
#include <mutex>
#include <thread>
std::mutex cout_mutex;    //方便多线程打印

class A{
public:
    A(){
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "create A" << std::endl;
    }

    ~A() {
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "destroy A" << std::endl;
    }

    int counter = 0;
    int get_value() {
        return counter++;
    }
};

void thread_func(const std::string& thread_name) {
    for (int i = 0; i < 3; ++i) {
        thread_local A* a = new A();
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "thread[" << thread_name << "]: a.counter:" << a->get_value() << std::endl;
    }
    return;
}

int main() {
    std::thread t1(thread_func, "t1");
    std::thread t2(thread_func, "t2");
    t1.join();
    t2.join();
    return 0;
}

输出:

create A
thread[t1]: a.counter:0
thread[t1]: a.counter:1
thread[t1]: a.counter:2
create A
thread[t2]: a.counter:0
thread[t2]: a.counter:1
thread[t2]: a.counter:2

可以看出类对象的使用和创建和局部变量类似,都不会创建多个。这种情况在函数间或通过函数返回实例也是一样的:

A* creatA() {
    return new A();
}

void loopin_func(const std::string& thread_name) {
    thread_local A* a = creatA();
    std::lock_guard<std::mutex> lock(cout_mutex);
    std::cout << "thread[" << thread_name << "]: a.counter:" << a->get_value() << std::endl;
    return;
}

void thread_func(const std::string& thread_name) {
    for (int i = 0; i < 3; ++i) {    
        loopin_func(thread_name);
    }
    return;
}

输出:

create A
thread[t1]: a.counter:0
thread[t1]: a.counter:1
thread[t1]: a.counter:2
create A
thread[t2]: a.counter:0
thread[t2]: a.counter:1
thread[t2]: a.counter:2

虽然 createA() 看上去被调用了多次,实际上只被调用了一次,因为thread_local 变量只会在每个线程最开始被调用的时候进行初始化,并且只会被初始化一次。

举一反三,如果不是初始化,而是赋值,则情况就不同了:

void thread_func(const std::string& thread_name) {
    for (int i = 0; i < 3; ++i) {
        thread_local A*  a;
        a = new A();
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "thread[" << thread_name << "]: a.counter:" << a->get_value() << std::endl;
    }
    return;
}

int main() {
    std::thread t1(thread_func, "t1");
    std::thread t2(thread_func, "t2");
    t1.join();
    t2.join();
    return 0;
}

输出

create A
thread[t1]: a.counter:0
create A
thread[t1]: a.counter:0
create A
thread[t1]: a.counter:0
create A
thread[t2]: a.counter:0
create A
thread[t2]: a.counter:0
create A
thread[t2]: a.counter:0



很明显,虽然只初始化一次,但却可以被多次赋值,因此 C++ 变量初始化是十分重要的

类成员变量

规定:thread_local 作为类成员变量时必须是 static 的

#include <iostream>
#include <mutex>
#include <thread>
std::mutex cout_mutex;    //方便多线程打印


class B {
public:
    B() {
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "create B" << std::endl;
    }
    ~B() {
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "destroy B" << std::endl;
    }
    thread_local static int b_key;
    //thread_local int b_key;
    int b_value = 24;
    static int b_static;
};
thread_local int B::b_key = 12;
int B::b_static = 36;
void thread_func(const std::string& thread_name) {
    B b;
    for (int i = 0; i < 3; ++i) {
        b.b_key--;
        b.b_value--;
        B::b_static--;   // not thread safe
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "thread[" << thread_name << "]: b_key:" << b.b_key << ", b_value:" << b.b_value << ", b_static:" << b.b_static << std::endl;
        std::cout << "thread[" << thread_name << "]: B::key:" << B::b_key << ", b_value:" << b.b_value << ", b_static: " << B::b_static << std::endl;
    }
    return;
}

int main() {
    std::thread t1(thread_func, "t1");
    std::thread t2(thread_func, "t2");
    t1.join();
    t2.join();
    return 0;
}

输出:

create B
thread[t2]: b_key:11, b_value:23, b_static:35
thread[t2]: B::key:11, b_value:23, b_static: 35
thread[t2]: b_key:10, b_value:22, b_static:34
thread[t2]: B::key:10, b_value:22, b_static: 34
thread[t2]: b_key:9, b_value:21, b_static:33
thread[t2]: B::key:9, b_value:21, b_static: 33
destroy B
create B
thread[t1]: b_key:11, b_value:23, b_static:32
thread[t1]: B::key:11, b_value:23, b_static: 32
thread[t1]: b_key:10, b_value:22, b_static:31
thread[t1]: B::key:10, b_value:22, b_static: 31
thread[t1]: b_key:9, b_value:21, b_static:30
thread[t1]: B::key:9, b_value:21, b_static: 30
destroy B

Process finished with exit code 0

b_key 是 thread_local,虽然其也是 static 的,但是每个线程中有一个,每次线程中的所有调用共享这个变量。b_static 是真正的 static,全局只有一个,所有线程共享这个变量。