C++ 的存储类型与新的 thread_local

news2025/1/17 0:59:37

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

  这里介绍一下 C++ 11 新补充的线程本地存储和 thread_local 存储类型说明符。线程存储周期是指对象在线程开始时创建,并在线程结束时销毁。thread_local 说明符用于声明一个变量是线程本地存储类型,对于这种类型的变量,每个线程都会维护一个该变量的实例,各个线程的变量实例之间互相不影响,这也就是线程本地存储的意义。thread_local 说明符可以和 static 或 extern 说明符一起使用,用来描述变量的链接属性是静态链接还是外部链接。

2.1 thread_local 应用例子

2.1.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 是静态存储周期,就没有这个特性,两个线程互相产生了影响。

2.1.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
2.1.3 thread_local 与 成员变量

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

2.1.4 thread_local 与初始化

  考虑一下下面的代码,想象会创建几个类 Foo 的实例?

struct Foo
{
    Foo(int a) { m_a = a; }
    int m_a;
};

for (int i = 0; i < 5; i++)
{
    thread_local Foo f(3);
    f.m_a++;
    std::cout << f.m_a;
}

答案是只有一个 Foo 的实例会被创建,并且 f(3) 的初始化在线程开始的时候做一次,最终打印的结果是 45678,并且 f 将在线程结束的时候被调用析构函数。如果不使用 thread_local,则会构造 5 个 Foo 的实例,并且销毁 5 次,最终打印的结果是 44444。

2.2 thread_local 的意义

  在 thread_local 提出之前,你无法为一个线程定义自己的全局变量(线程级别的全局变量),只能将全局变量定义在父进程中,由所有的线程(不同种类的线程)共享使用(操作系统可能会提供线程局部存储之类的支持,但那不是 C++ 语言规范层面的东西)。但是当程序复杂到一定程度的时候,线程之间的串扰就在所难免,同时也增大了多线程编码的复杂度。前面的例子展示了 thread_local 的用法,每个线程共享一个属于本线程的变量的实例,相当于线程有了自己的全局变量。

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

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

关注公众号,与作者互动:
在这里插入图片描述## 参考资料

[1] Marc Gregoire, Professional C++ (Fifth Edition), John Wiley & Sons, Inc., 2021
[2] Scott Meyers, Effective Modern C++, O’Reilly, 2015
[3] Bjarne Stroustrup. The C++ Programming Language, Fourth Edition. Pearson. 2013(中文版:机械工业出版社)
[4] Bartłomiej Filipek. C++17 In Detail. Leanpub. 2019
[5] Nicolai M. Josuttis, C++20 - The Complete Guide, http://leanpub.com/cpp20’
[6] Jacek Galowicz. C++17 STL Cookbook. Packtpub. 2017
[7] https://en.cppreference.com/w/c/thread/thread_local

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2214523.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【黑马点评】项目知识点及面经整理

【黑马点评】项目知识点及面经整理 1 短信登录&#xff08;Session&#xff0c;Redis&#xff0c;JWT验证&#xff09;1.1 JWT和Session的区别1.2 Session1.3 Redis1.3.1 基于Redis实现登录验证1.3.2 登录拦截 1.4 JWT1.4.1 JWT的有效验证1.4.2 JWT定义 2 热点数据缓存2.1 缓存…

区块链-智能合约Solidity编程

文章目录 一、ubuntu安装二、FISCO BCOS安装五、 WeBASE安装5.1 WeBASE简介5.2 节点前置服务搭建5.3 调用HelloWorld合约 七、Solidity极简入门7.1. 值类型7.2. 变量数据存储和作用域7.3. 函数7.4 控制流7.5 数组&映射7.6 结构体7.7 修饰符7.8 事件7.9 面向对象7.10 抽象合…

SoC芯片中Clock Gen和Reset Gen的时钟树综合

社区目前已经开设了下面列举的前四大数字后端实战课程&#xff0c;均为直播课&#xff0c;且均是小编本人亲自授课&#xff01;遇到项目问题&#xff0c;都可以远程一对一指导解决具体问题。小编本人是一线12年后端经验的数字后端工程师。想找一线IC后端技术专家亲自带你做后端…

Java Fork-Join框架学习

概述 Fork/Join是Java7提供的一个用于并行执行任务的框架&#xff0c;是一个把大任务分割成若干个小任务&#xff0c;最终汇总每个小任务结果后得到大任务结果的框架。Fork负责把一个大任务切分为若干并行执行的子任务&#xff0c;Join负责合并这些子任务的执行结果&#xff0…

Ubuntu系衍生版手动修改配置网卡的配置总结

一、Ubuntu系的IP地址配置文件的目录&#xff1a; sudo vim /etc/network/interfaces 二、以DHCP方式配置网卡&#xff1a; 在以上配置文件中添加以下两行&#xff1a; auto enp3s0 iface enp3s0 inet dhcp 三、为网卡配置静态IP地址&#xff1a; 在以上配置文件中添…

实验3,网络地址转换

实验3&#xff1a;网络地址转换 实验目的及要求&#xff1a; 通过实验&#xff0c;掌握NAT技术的工作原理&#xff0c;了解三种不同类型NAT技术的主要作用以及各自的主要应用环境。能够完成静态NAT和复用NAT技术的应用&#xff0c;并熟练掌握NAT技术相关的配置命令。 实验设…

el-date-picker选择时间后标准时间少1小时问题

问题 前端开发中发现Element的时间组件el-date-picker在选择选择部分时间后js对象的标准时间少1小时&#xff0c;如果选择的小时为0&#xff0c;会导致部分转换条件下结果少1天。 比如组件中选择的本地时间为&#xff1a; 1988-08-01 00:00:00 而js对象获取到是标准时间是&am…

ubuntu 安装kali命令补全功能

输入命令时&#xff0c;之前的命令会以阴影显示&#xff0c;按下右键或 Tab 键可以直接补全 安装zsh-autosuggestions sudo apt install zsh-autosuggestions编辑 ~/.zshrc环境变量 if [ -f /usr/share/zsh-autosuggestions/zsh-autosuggestions.zsh ]; then. /usr/share/zs…

【从零开始的LeetCode-算法】945. 使数组唯一的最小增量

给你一个整数数组 nums 。每次 move 操作将会选择任意一个满足 0 < i < nums.length 的下标 i&#xff0c;并将 nums[i] 递增 1。 返回使 nums 中的每个值都变成唯一的所需要的最少操作次数。 生成的测试用例保证答案在 32 位整数范围内。 示例 1&#xff1a; 输入&am…

【Hadoop】HDFS基本操作

参考&#xff1a;3.HDFS基本操作_哔哩哔哩_bilibili 创建目录 hadoop fs -mkdir -p /training/qiang查看当前根目录下文件 hadoop fs -ls /hadoop fs -ls /training/目录授权 hadoop fs -chmod -R 777 /training/qm777是最大权限&#xff0c;读写 4、2、1 上传文件 先创…

YZ系列工具之YZ09:VBA_Excel之读心术

我给VBA下的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。我的教程一共九套一部VBA手册&#xff0c;教程分为初级、中级、高级三大部分。是对VBA的系统讲解&#xff0c;从简单的…

Python自动化脚本裁剪图片为1:1比例

一、创建输入文件夹&#xff08;in&#xff09;和输出文件夹&#xff08;out&#xff09;&#xff0c;将原始图片放在输入文件夹&#xff08;in&#xff09;里 二、 安装对应的Python库 pip install Pillow 三、编写自动化脚本代码 import os from PIL import Imagedef crop…

Axure RP电商系统商城PC+app+后台买家卖端高保真原型模板及元件库

AxureRP电商商城PCapp后台买家卖端高保真原型模板本套包含三份原型图素材 APP买家端原型简介&#xff1a; 包含了用户中心、会员成长、优惠券、积分、互动社区、运营推广、内容推荐、商品展示、订单流程、订单管理、售后及服务等完整的电商体系功能架构和业务流程。 本模板由…

Spring中ApplicationEvent事件的实现

1&#xff09;简介 Spring Event (接口名为Aplication Event) 观察者设计模式&#xff0c;由事件发起者publisher发布事件&#xff08;指定事件名&#xff09;&#xff0c;事件监听者监听事件&#xff08;指定事件名&#xff09;。 好比&#xff0c; A 说了一句话&#xff0c…

安卓14无法安装应用解决历程

客户手机基本情况&#xff1a; 安卓14&#xff0c;对应的 targetSdkVersion 34 前天遇到了安卓14适配问题&#xff0c;客户发来的截图是这样的 描述&#xff1a;无法安装我们公司的B应用。 型号&#xff1a;三星google美版 解决步骤&#xff1a; 1、寻找其他安卓14手机测试…

从源码到平台:使用视频美颜SDK构建高性能直播美颜系统详解

本文将深入探讨如何从源码出发&#xff0c;借助视频美颜SDK构建一套高性能的直播美颜系统&#xff0c;涵盖技术架构、核心功能的实现以及性能优化等方面的详解。 一、视频美颜SDK的作用与选择 视频美颜SDK是开发直播美颜系统的基础&#xff0c;它能够提供实时美颜处理功能&am…

STM32的GPIO片上外设

一、STM32的片上外设 片上外设是集成在 MCU 芯片内部的硬件模块&#xff0c;它们通过片内总线与 CPU 直接通信&#xff0c;而不是通过外部引脚或接口连接的设备。 片上外设&#xff1a; 包括 UART、I2C、SPI、ADC、定时器、GPIO、DMA、RTC 等集成在 MCU 内部的模块。这些外设…

《Linux从小白到高手》综合应用篇:深入理解Linux磁盘及IO优化

1. 前言 其实磁盘优化和IO优化&#xff0c;我在前面的其他Linux调优博文中已经讲述过或者涉及过了&#xff0c;但是太过零碎&#xff0c;所以本篇就来集中深入讨论下Linux磁盘和IO调优。 2.磁盘调优 结合我多年的经验&#xff0c;本人认为磁盘调优最重要的是读写性能的提升和…

Unity 实战案例全解析 实现时间停止效果+世界变灰

画面里运动的那个小玩意这么写 using System.Collections; using System.Collections.Generic; using UnityEngine;public class Partol : MonoBehaviour {public Transform pos1;public Transform pos2;public float speed;private Transform target;void Start() {target p…

SpringBoot智能推荐:健康生活新选择

3系统分析 3.1可行性分析 通过对本基于智能推荐的卫生健康系统实行的目的初步调查和分析&#xff0c;提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本基于智能推荐的卫生健康系统采用SSM框架&#…