C++11 thread_local的 用法(一)

简介: C++11 thread_local的 用法(一)

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


thread_local 简介


thread_local 是一个存储器指定符。

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

  • auto:自动存储期;
  • register:自动存储期,提示编译器将此变量置于寄存器中;
  • static:静态或线程存储期,同时提示是内部链接;
  • extern:静态或线程存储期,同时提示是外部链接;
  • thread_local:线程存储期;
  • mutable:不影响存储期或链接。

对于 thread_local,官方解释是:

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

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

这里有一个很重要的信息,就是 static thread_localthread_local 声明是等价的,都是指定变量的周期是在线程内部,并且是静态的。这是什么意思呢?举个代码的例子。

下面是一个线程安全的均匀分布随机数生成,


inline void random_uniform_float(float *const dst, const int len, const int min = 0, const int max = 1)
{
    // generator is only created once in per thread, but distribution can be regenerated.
    static thread_local std::default_random_engine generator;     // heavy
    std::uniform_real_distribution<float> distribution(min, max); // light
    for (int i = 0; i < len; ++i)
    {
        dst[i] = distribution(generator);
    }
}


generator 是一个函数的静态变量,理论上这个静态变量在函数的所有调用期间都是同一个的(静态存储期),相反 distribution 是每次调用生成的函数内临时变量。现在 generator 被 thread_local 修饰,表示其存储周期从整个函数调用变为了线程存储期,也就是在同一个线程内,这个变量表现的就和函数静态变量一样,但是不同线程中是不同的。可以理解为 thread_local 缩小了变量的存储周期。关于 thread_local 变量自动 static,C++ 标准中也有说明:

When thread_local is applied to a variable of block scope the storage-class-specifier static is implied if it does not appear explicitly

关于 thread_local 的定义我也不想过多着墨,还是看代码例子说明吧。


thread_local 使用示例


全局变量


#include  <iostream>
#include <thread>
#include <mutex>
std::mutex cout_mutex;    //方便多线程打印
thread_local int x = 1;
void thread_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;
    }
    return;
}
int main() {
    std::thread t1(thread_func, "t1");
    std::thread t2(thread_func, "t2");
    t1.join();
    t2.join();
    return 0;
}


上面的代码在主函数中,创建了两个线程 t1 和 t2,并分别传递了一个字符串作为线程名,以便在线程函数中输出。接着,使用 join() 方法等待线程执行完毕。

在线程函数 thread_func 中,每个线程将 x 的值增加三次,然后输出结果。由于 x 是 thread_local 变量,每个线程对它的操作互不干扰,因此输出结果是不同的。由于 cout 是共享的,使用 std::lock_guardstd::mutex lock(cout_mutex) 锁住 cout,以避免多个线程同时输出内容时出现乱序或重叠的问题。

输出:


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


可以看出全局的 thread_local 变量在每个线程里是分别自加互不干扰的。


局部变量


#include  <iostream>
#include <thread>
#include <mutex>
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;
    }
    return;
}
int main() {
    std::thread t1(thread_func, "t1");
    std::thread t2(thread_func, "t2");
    t1.join();
    t2.join();
    return 0;
}


上面这段代码用了 STL 库中的 thread、mutex 等类,可以实现并发执行多个线程。代码中定义了一个 thread_func 函数,其参数为一个字符串 thread_name,表示线程的名字。函数中使用了一个 thread_local 关键字声明的变量 x,该变量只在当前线程中可见。在每次循环中,x 都会加 1,并使用互斥锁保证输出时的线程安全。在主函数中,创建了两个线程 t1 和 t2 分别执行 thread_func 函数,并使用 join 函数等待两个线程执行完毕后再退出程序。

输出:


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


可以看到虽然是局部变量,但是在每个线程的每次 for 循环中,使用的都是线程中的同一个变量,也侧面印证了 thread_local 变量会自动 static

如果我们不加 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


体现了局部变量的特征。

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


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;
    }
    x++;    //编译会出错:error: ‘x’ was not declared in this scope
    return;
}


上面的代码中,thread_func函数被作为线程函数传递给std::thread类的构造函数。在thread_func函数中,有一个for循环,循环体内声明了一个thread_local变量xthread_local关键字用于声明一个线程局部变量,即该变量在每个线程中都有一份独立的实例,且在不同线程中互相独立。所以每个线程中的x都是独立的,不会互相影响。

在循环体内部,对x进行自增操作,并使用std::lock_guard保护打印输出,以避免并发操作导致的数据竞争问题。然后打印输出x的值和线程名。

在循环结束后,试图对x进行自增操作。但是由于在这个位置,x的作用域已经结束了,编译器会报错:error: ‘x’ was not declared in this scope。所以这行代码是不合法的。

目录
相关文章
|
6天前
|
Linux Shell 开发工具
C++ 的 ini 配置文件读写/注释库 inicpp 用法 [ header-file-only ]
这是一个C++库,名为inicpp,用于读写带有注释的INI配置文件,仅包含一个hpp头文件,无需编译,支持C++11及以上版本。该库提供简单的接口,使得操作INI文件变得容易。用户可通过`git clone`从GitHub或Gitee获取库,并通过包含`inicpp.hpp`来使用`inicpp::iniReader`类。示例代码展示了读取、写入配置项以及添加注释的功能,还提供了转换为字符串、双精度和整型的函数。项目遵循MIT许可证,示例代码可在Linux环境下编译运行。
37 0
|
6天前
|
安全 程序员 C++
C++ new和delete的用法
需要注意的是,使用 `new`和 `delete`分配和释放内存时,程序员负责管理内存的分配和释放,这可能导致内存泄漏或释放已释放内存的问题。因此,C++引入了智能指针(如 `std::shared_ptr`和 `std::unique_ptr`)以更安全和自动化地管理内存。
36 2
|
6天前
|
编译器 C++
【C++】【C++的常变量取地址问题(对比C的不同)】const修饰的常变量&volatile修饰用法详解(代码演示)
【C++】【C++的常变量取地址问题(对比C的不同)】const修饰的常变量&volatile修饰用法详解(代码演示)
|
6天前
|
C++
C++ 默认参数与引用传递:语法、用法及示例
C++ 允许函数参数具有默认值,简化调用。例如,`void myFunction(string country = &quot;Norway&quot;)` 中`country`默认为&quot;Norway&quot;。默认参数仅适用于函数参数,不包括返回值。引用传递是另一种传递方式,函数直接访问变量内存,允许修改原值,提高效率。`void swapNums(int &x, int &y)` 中`x`和`y`为引用参数。了解这些特性可提升代码可读性和性能。
45 0
|
6天前
|
人工智能 安全 机器人
【C++】dynamic_cast基本用法(详细讲解)
【C++】dynamic_cast基本用法(详细讲解)
|
6天前
|
人工智能 安全 机器人
【C++】const_cast基本用法(详细讲解)
【C++】const_cast基本用法(详细讲解)
|
6天前
|
人工智能 机器人 测试技术
【C++】static_cast基本用法(详细讲解)
【C++】static_cast基本用法(详细讲解)
|
6天前
|
人工智能 机器人 中间件
【C++】C++回调函数基本用法(详细讲解)
【C++】C++回调函数基本用法(详细讲解)
|
6天前
|
算法 安全 编译器
【C++ 17 新特性 折叠表达式 fold expressions】理解学习 C++ 17 折叠表达式 的用法
【C++ 17 新特性 折叠表达式 fold expressions】理解学习 C++ 17 折叠表达式 的用法
36 1
|
6天前
|
存储 算法 编译器
【C++ TypeName用法 】掌握C++中的TypeName:模板编程的瑞士军刀
【C++ TypeName用法 】掌握C++中的TypeName:模板编程的瑞士军刀
239 0