C++多线程:thread_local

  • Post author:
  • Post category:其他




概念

首先

thread_local

是一个关键词,

thread_local

是C++ 11新引入的一种存储期指定符。它会影响变量的存储周期(Storage duration),与它同是存储期指定符的还有以下几个:

关键字 说明 备注
auto 自动存储期 c++11前, “auto int x; ” 在c++11起错误
register 自动存储期。指示编译器将此对象置于处理器的寄存器中。 c++17弃用
static 静态或者线程存储期的内部链接
extern 静态或者线程存储期的外部链接
thread_local 线程存储期 c++11起
mutable 不影响存储期或链接


thread_local

指示对象拥有线程存储期。也就是对象的存储在线程开始时分配,而在线程结束时解分配。每个线程拥有其自身的对象实例。唯有声明为

thread_local

的对象拥有此存储期。 thread_local 能与 static 或 extern 结合一同出现,以调整链接(分别指定内部或外部链接),详细的可以查阅:

存储类说明符 – cppreference.com


thread_local

关键词只对声明于命名空间作用域的对象、声明于块作用域的对象及静态数据成员允许。举例如下:

thread_local int x;  // 1 A thread-local variable at namespace scope
class X
{
    static thread_local std::string s; // 2 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;  // 3 A thread-local local variable
}

下面我们来具体学习一下

thread_local

的这几种应用场景。



全局变量

#include <iostream>
#include <thread>
#include <mutex>

std::mutex cout_mutex;    // 用于多线程打印
thread_local int x = 1;

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

int main() {
    std::thread t1(func, "t1");
    std::thread t2(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

从上输出也确实能看出,每个线程都有自己单独的x副本,互不干预。



局部变量

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

void 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(func, "t1");
    std::thread t2(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

的局部变量没有因为for循环作用域而重新赋值。这是因为线程存储期的变量都是和线程绑定的,所以只有第一次声明时被赋值。可以理解为线程专用的static变量。不过变量的作用域依然是在本身的作用域内。比如:在for循环外使用x就会编译时错误。



类对象

#include <iostream>
#include <thread>
#include <mutex>
std::mutex cout_mutex;

class A {
public:
    A() {
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "initialize 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 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;
    }
}

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

输出:

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

可以看出虽然在循环中创建了A的实例a,但是并没有因为循环创建了多个。这个与局部变量的情况相同,创建的实例相对于thread是static的。

但是如果没有在声明时进行赋值,就不一样了。如:

#include <iostream>
#include <thread>
#include <mutex>
std::mutex cout_mutex;

class A {
public:
    A() {
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "initialize 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 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(func, "t1");
    std::thread t2(func, "t2");
    t1.join();
    t2.join();
    return 0;
}

输出:

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

很好理解,在循环执行时,每次都创建了一个A实例并对a进行赋值。所有一般情况要求我们:

thread_local对象声明时赋值



类成员变量


thread_local

作为类成员变量时必须是static的,修改代码:

#include <iostream>
#include <thread>
#include <mutex>
std::mutex cout_mutex;

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

    thread_local static int key_;
    int value_ = 24;
    static int static_;
};
int A::static_ = 36;
thread_local int A::key_ = 12;


void func(const std::string& thread_name) {
    A aa;
    for (int i = 0; i < 3; ++i) {
        aa.key_--;
        aa.value_--;
        aa.static_--;
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "thread[" << thread_name << "]: key_:" << aa.key_
            << ", value_:" << aa.value_ << ", static_:" << aa.static_ << std::endl;
        std::cout << "thread[" << thread_name << "]: A::key_:" << A::key_
            << ", value_:" << aa.value_ << ", static_: " << A::static_ << std::endl;
    }
    return;
}

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

输出:

initialize A
thread[t1]: key_:11, value_:23, static_:35
thread[t1]: A::key_:11, value_:23, static_: 35
thread[t1]: key_:10, value_:22, static_:34
thread[t1]: A::key_:10, value_:22, static_: 34
thread[t1]: key_:9, value_:21, static_:33
thread[t1]: A::key_:9, value_:21, static_: 33
destroy A
initialize A
thread[t2]: key_:11, value_:23, static_:32
thread[t2]: A::key_:11, value_:23, static_: 32
thread[t2]: key_:10, value_:22, static_:31
thread[t2]: A::key_:10, value_:22, static_: 31
thread[t2]: key_:9, value_:21, static_:30
thread[t2]: A::key_:9, value_:21, static_: 30
destroy A

从上面例子可以看出

thread_local

作为类成员时也是对于每个thread分别分配了一个。而static则是全局一个。



其他

本质上

thread_local

修饰后仍然是一个变量,我们依旧能够使用取地址操作者通过引用的方法传递给其他线程对其进行修改:

#include <iostream>
#include <thread>

thread_local int i=0;

void func(int* p){
    *p = 42;
}

int main(){
    i = 9;
    std::thread t(func, &i);
    t.join();
    std::cout << i << std::endl;
}

程序将输出

42

另外,

thread_local

变量在第一次使用时初始化,如果变量(类)没有被使用。此变量(类)将不会被初始化:

#include <iostream>
#include <thread>

struct A {
    A() {
        std::cout<< "initialized A" << std::endl;
    }
    ~A() {
        std::cout << "deleted A" << std::endl;
    }
    int i;
};

thread_local my_class ss;

void do_nothing() {
}

int main(){
    std::thread t1(do_nothing);
    t1.join();
}



总结

thread-local storage 和

static

(或者说

global

) 存储很类似,每一个线程都将拥有一份这个数据的拷贝,

thread_local

对象的生命周期从线程开始时开始(对于全局变量),或者首先分配空间。当线程退出的时候对象析构;

一般在声明时赋值,在本thread中只执行一次。当用于类成员变量时,必须是static的。

参考:


C++11 thread_local用法 – 知乎 (zhihu.com)


存储类说明符 – cppreference.com



版权声明:本文为iuices原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。