C++之thread_local变量

流星雨爱编程 2024-06-26 08:05:35 阅读 59

目录

1.C++ 的存储类型

1.1.存储周期(Storage duration)

1.2.存储类型说明符(Storage class specifiers)

1.3.存储类型说明符与存储周期的关系

2.thread_local简介

3.thread_local 应用

3.1.thread_local 与全局变量

3.2.thread_local 与 static变量

3.3.thread_local 与 成员变量

3.4.thread_local 与初始化

4.thread_local 的用处

5.性能考虑

6.替代方案

7.总结


1.C++ 的存储类型

1.1.存储周期(Storage duration)

        存储周期表示一个变量的存储空间持续的时间,它应该与对象的语义生命周期一致(或至少不小于对象的语义生命周期)。C++ 98从 C 继承了三种存储周期,分别是静态存储周期(static storage duration)、自动存储周期(automatic storage duration)和动态存储周期(dynamic storage duration),C++ 11 又增加了一种线程存储周期(thread storage duration)。

        存储周期只是一个概念,是程序语义范畴内的东西,但不是语法的范畴。这个概念在语法上的表示则由下一节介绍的存储类型说明符(Storage class specifiers)展示。

1.2.存储类型说明符(Storage class specifiers)

        存储类型说明符(Storage class specifiers)也被称为存储类型,它们是变量声明语法中类型说明符的一部分,它们和变量名的范围一起控制变量的两个独立属性,即存储周期(storage duration)和链接属性( linkage)。C++ 98从 C 语言继承了 auto、register、static 和 extern 四种类型,同时补充了一种 mutable,C++ 11 针对线程存储周期又增加了一个线程本地存储的说明符 thread_local。关于这几个存储类型说明符的作用,请参考下表:

类型 说明 备注
auto 自动存储周期,也是变量的默认存储类型,由变量的域范围决定变量的存储周期,比如局部变量的存储周期随着域的结束而结束,而全局变量的存储周期则与程序的运行时间一致 从 C++11 开始,显示使用 auto 存储类型会导致编译错误。比如 auto int i; 会导致编译错误
register 也是自动存储类型,不过暗示编译器会择机将其放置在寄存器中以提高数据存取的效率 在 C++ 17 被移除标准,以后应避免使用这个存储类型
static 静态或线程存储周期,采用内部链接(对于不属于匿名名字空间(anonymous namespace)的静态类成员,采用外部链接) static 表示一个对象具有静态存储持续周期。它的生命周期是程序的整个执行过程,其存储的值在程序启动之前只初始化一次
extern 静态或线程存储周期,采用外部链接
mutable 严格来说,这不是一种存储类型,因为它既不影响变量的存储周期,也不影响链接属性,它只是表示一种可以“不动声色”地修改常量对象成员的机会。
thread_local 线程存储类型

1.3.存储类型说明符与存储周期的关系

C++ 中变量存储周期与变量类型说明符的关系如下表所示:

存储周期 变量类型与类型说明符
自动存储周期 显式使用 register 声明的变量,或隐式声明为 static 或 extern 的作用域内部变量,没有明确指定存储类型说明符的变量
静态存储周期 1、非 thread_local 声明的全局(非局部)变量;2、非动态生成(使用 new 创建)的非局部变量;3、用 static 声明的局部变量、全局变量和类成员变量
动态存储周期 1、使用 new 表达式创建(非 placement_new),并且使用 delete 销毁的对象;2、使用其他动态分配函数和动态释放函数管理的对象存储位置
线程存储周期 使用 thread_local 声明的所有变量,包括局部变量、全局变量和成员变量

2.thread_local简介

        thread_local 是 C++11 为线程安全引进的变量声明符。表示对象的生命周期属于线程存储期。

        线程局部存储(Thread Local Storage,TLS)是一种存储期(storage duration),对象的存储是在线程开始时分配,线程结束时回收,每个线程有该对象自己的实例;如果类的成员函数内定义了 thread_local 变量,则对于同一个线程内的该类的多个对象都会共享一个变量实例,并且只会在第一次执行这个成员函数时初始化这个变量实例。

        thread_local 一般用于需要保证线程安全的函数中。本质上,就是线程域的全局静态变量。

3.thread_local 应用

3.1.thread_local 与全局变量

        使用 thread_local 声明的变量会在每个线程中维护一个该变量的实例,线程之间互不影响,这里我们用一个普通的全局变量和一个 thread_local 类型的全局变量做对比,说明一下这种存储类型的变量有什么性质。

std::mutex print_mtx; //避免打印被冲断

thread_local int thread_count = 1;

int global_count = 1;

void ThreadFunction(const std::string& name, int cpinc)

{

for (int i = 0; i < 5; i++)

{

std::this_thread::sleep_for(std::chrono::seconds(1));

std::lock_guard<std::mutex> lock(print_mtx);

std::cout << "thread name: " << name << ", thread_count = " << thread_count

<< ", global_count = " << global_count++ << std::endl;

thread_count += cpinc;

}

}

int main()

{

std::thread t1(ThreadFunction, "t1", 2);

std::thread t2(ThreadFunction, "t2", 5);

t1.join();

t2.join();

}

输出:

thread name: t2, thread_count = 1, global_count = 1

thread name: t1, thread_count = 1, global_count = 2

thread name: t1, thread_count = 3, global_count = 3

thread name: t2, thread_count = 6, global_count = 4

thread name: t1, thread_count = 5, global_count = 5

thread name: t2, thread_count = 11, global_count = 6

thread name: t1, thread_count = 7, global_count = 7

thread name: t2, thread_count = 16, global_count = 8

thread name: t1, thread_count = 9, global_count = 9

thread name: t2, thread_count = 21, global_count = 10

        可以看出来每个线程中的 thread_count 都是从 1 开始打印,这印证了 thread_local 存储类型的变量会在线程开始时被初始化,每个线程都初始化自己的那份实例。另外,两个线程的打印数据也印证了 thread_count 的值在两个线程中互相不影响。作为对比的 global_count 是静态存储周期,就没有这个特性,两个线程互相产生了影响。

3.2.thread_local 与 static变量

        thread_local 也可以用于局部变量的声明,其作用域的约束与局部静态变量类似,但是其存储与局部静态变量不一样,首先是每个线程都有自己的变量实例,其次是其生命周期与线程一致,而局部静态变量的声明周期是直到程序结束。下面再用一个例子演示一下:

void DoPrint(const std::string& name, int cpinc)

{

static int static_count = 1;

thread_local int local_count = 1;

std::cout << "thread name: " << name << ", local_count = " << local_count

<< ", static_count = " << static_count++ << std::endl;

local_count += cpinc;

}

void ThreadFunction(const std::string& name, int cpinc)

{

for (int i = 0; i < 5; i++)

{

std::this_thread::sleep_for(std::chrono::seconds(1));

std::lock_guard<std::mutex> lock(print_mtx);

DoPrint(name, cpinc);

}

}

int main()

{

std::thread t1(ThreadFunction, "t1", 2);

std::thread t2(ThreadFunction, "t2", 5);

t1.join();

t2.join();

}

        在上面的例子中,static_count 和 local_count 变量的作用域都仅限于 DoPrint() 函数内部,但是存储类型不一样,local_count 在每个线程中的实例独立初始化,独立变化,线程之间没有影响,而局部静态变量 static_count 则在两个线程之间互相影响。从结果打印的情况也印证了这一点:

thread name: t1, local_count = 1, static_count = 1

thread name: t2, local_count = 1, static_count = 2

thread name: t1, local_count = 3, static_count = 3

thread name: t2, local_count = 6, static_count = 4

thread name: t2, local_count = 11, static_count = 5

thread name: t1, local_count = 5, static_count = 6

thread name: t1, local_count = 7, static_count = 7

thread name: t2, local_count = 16, static_count = 8

thread name: t1, local_count = 9, static_count = 9

thread name: t2, local_count = 21, static_count = 10

3.3.thread_local 与 成员变量

        thread_local 可以用于类的成员变量,但是只能用于静态成员变量。这很容易理解,C++ 不能在对象只有一份拷贝的情况下弄出多个成员变量的实例,但是静态成员就不一样了,每个类的静态成员共享一个实例,改成线程局部存储比较容易实现,也容易理解。

class B {

public:

B() {

std::lock_guard<std::mutex> lock(cout_mutex);

std::cout << "create B" << std::endl;

}

~B() {}

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;

}

输出:

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

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

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

3.4.thread_local 与初始化

#include <iostream>

#include <thread>

#include <mutex>

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++;

}

};

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;

}

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

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

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

void loopin_func(const std::string& thread_name) {

thread_local A* a;

a = creatA();

std::lock_guard<std::mutex> lock(cout_mutex);

std::cout << "thread[" << thread_name << "]: a.counter:" << a->get_value() << std::endl;

return;

}

输出:

create A

thread[t1]: a.counter:0

thread[t1]: a.counter:0

thread[t1]: a.counter:0

create A

thread[t2]: a.counter:0

thread[t2]: a.counter:0

thread[t2]: a.counter:0

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

4.thread_local 的用处

        在 thread_local 提出之前,你无法为一个线程定义自己的全局变量(线程级别的全局变量),只能将全局变量定义在父进程中,由所有的线程(不同种类的线程)共享使用。但是当程序复杂到一定程度的时候,线程之间的串扰就在所难免,同时也增大了多线程编码的复杂度。前面的例子展示了 thread_local 的用法,每个线程共享一个属于本线程的变量的实例,相当于线程有了自己的全局变量。

        另一个常用来解释 thread_local 的意义的例子就是随机数的生成。我们知道的随机数生成器都是伪随机数生成器,其随机性取决于种子(seed)的变化。如果一个函数使用局部变量设置随机数发生器的种子,那么它在每个使用这个函数的线程中都会被初始化,由于使用了相同的种子,每个线程将得到一样的随机数序列,这就使得多线程也不那么随机了。如果使用 thread_local 类型的种子,则每个线程维护自己的种子,从而使得每个线程都能得到不同的随机数序列,真正起到随机数的作用。

        其他的例子就是线程不安全问题,C 标准库的错误码 errno,还有 strtok() 等函数就是线程不安全的例子。有了 thread_local ,就可以用很小的改动解决这些函数的线程不安全问题。也不需要像有些编译器那样,专门提供一套线程安全的标准库,用过的人都知道,很多函数的参数定义都是不兼容的,对现有代码的改造成本非常高。

5.性能考虑

虽然 thread_local 变量提供了线程间的数据隔离,但它们也可能带来一些性能开销。访问 thread_local 变量通常比访问常规的全局变量或栈变量要慢,因为需要进行额外的 TLS 查找操作。因此,在性能敏感的代码中应谨慎使用 thread_local

6.替代方案

如果不需要真正的线程局部存储,但只是想在线程之间传递数据,可以考虑使用线程特定的数据(Thread-Specific Data, TSD)机制,如 POSIX 的 pthread_key_create 和 pthread_setspecific 函数。对于跨平台的应用程序,可以使用第三方库(如 Boost.Thread)来提供类似的功能。

7.总结

  thread_local 关键字为 C++ 程序员提供了一种方便的方式来处理多线程环境中的线程特定数据。通过避免数据竞争和简化同步机制,它可以帮助提高多线程程序的性能和可维护性。然而,在使用时需要注意其性能开销和跨平台兼容性问题,并根据具体场景选择合适的替代方案。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。