C++ 中的 volatile 和 atomic

0. TL;DR

  • std::atomic 用于多线程并发场景,有两个典型使用场景:

    • 原子操作:对 atomic 变量的操作(读/写/自增/自减)仿佛受互斥量保护。一般通过特殊的机器指令实现,比使用互斥量更高效
    • 限制编译器/硬件对赋值操作的重新排序
  • volatile 和多线程并发没有任何关系,用于防止编译器优化掉对特殊变量的“冗余”读写操作

1. Data Race 和未定义行为

C++ 中有很多未定义行为,Data Race 便是其中之一。

️ 如果一个变量不是 atomic 变量,且没有互斥量保护,在一个线程中执行写操作,同时在另一个线程中读取,则会产生 Data Race,其行为未定义!

例如在执行下面代码的过程中,另一个线程同时读取 i 的值,读到的值可能是 -13,42,0,43923... 任何值!

int i = 0;
++i;
--i;

虽然在这种场景下,你读到的大概率是0/1,但要知道,对于未定义行为,理论上可能读到任何值!!

2. atomic

std::atomic 是 C++ 中的模版类,一般用于 bool、整型、指针类型,如 atomic<bool>atomic<int>atomic<Widget*> 等。对 atomic 变量的操作(读/写/自增/自减)仿佛受互斥量保护(底层一般通过特殊的机器指令实现,比使用互斥量更高效)。

2.1 原子操作

atomic 的第一个应用场景就是多线程读写变量

atomic<int> ai {0};
ai = 10; // 原子写
std::cout << ai; // 原子读
++ai; // 原子自增为11
--ai; // 原子自减为10

执行上述代码期间,如果在另一个线程读取 ai 的值,只可能读到 0/10/11,不可能有其他结果

2.2 限制重排序

atomic 的第二个应用场景是,当某个变量在两个任务之间传递信息时,防止对该变量赋值进行重新排序。假设 a、b、x、y 是 4 个独立的变量:

a = b;
x = y;

为了提升性能,编译器可能会对不相关的赋值进行重新排序为:

x = y;
a = b;

即使编译器不这么做,底层硬件也可能这么做。atomic 可以避免这种重排序。例如在一个任务中计算 value,另一个任务中依赖 value 的可用性,则可以借助 atomic 变量实现:

atomic<bool> valueAvailable(false);
auto value = computeValue();
valueAvailable = true;

std::atomic 默认采用顺序一致性模型,会限制重新排序:不仅编译器会保证 value 在 valueAvailable 之前赋值,底层硬件也保证这个顺序。

️ C++ 还提供了其他更灵活的一致性模型(如 memory_order_relaxed),除非你是这方面的专家,很清楚不同内存序产生的影响,否则不要轻易使用。

2.3 load/store

有的开发者喜欢使用 load()/store() 成员函数,这不是必须,但可以起到强调作用:

  • 强调该变量涉及多线程并发操作
  • 强调 atomic 变量可能导致性能问题
    • 虽然 atomic 比互斥量更高效,但仍然比普通变量慢、开销大
    • atomic 变量可能会阻止重新排序优化

3. volatile

如果将上面的例子中的 atomic<bool> 换成 volatile bool,既无法保证操作的原子性,也无法限制对 value 和 valueAvailable 赋值的重新排序。

volatile bool valueAvailable(false);
auto value = computeValue();
valueAvailable = true;

对于普通内存来说,如果向某个内存写一个值,该值会一直保留在内存,直到被下一个写操作覆盖。编译器可以对普通内存的变量读写进行优化,例如下面这段代码(虽然一般开发者不会直接写出这样的代码,但是经过模版实例化、内联、重排序等优化后,很可能产生类似的代码):

int x;
auto y = x; // 读取 x
y = x; // 再次读取 x
x = 10; // 写入 x
x = 20; // 再次写入 x

可能被优化为:

auto y = x;
x = 20;

但是在有些特殊场景下(例如和外设交互),变量对应特殊的内存区域:

  • x 可能是一个传感器的值,连续两次读取 x 的值可能不同,不应该被优化

  • x 可能对应某个无线电发射器的控制端口,x = 10x = 20 对应两条不同的指令,不应该被优化

volatile 的作用正是告诉编译器,某个变量所对应内存是特殊内存,不要进行任何优化!

将 x 声明为 volatile int x 即可避免上述优化,而声明为 atomic<int> x 则没有这个效果。

️ 经常听到一种说法:开启编译器优化选项之后,会导致程序行为异常。但通常都不是编译器的问题,而是自己的代码不规范,或依赖了未定义行为导致的!

思考:

  • volatile 和 atomic 可以同时使用吗,用于什么场景?
  • volatile 和 const 可以同时使用吗,用于什么场景?

4. Reference

《Effective Morden C++》条款 40

C++ 中的 volatile 和 atomic的更多相关文章

  1. zz剖析为什么在多核多线程程序中要慎用volatile关键字?

    [摘要]编译器保证volatile自己的读写有序,但由于optimization和多线程可以和非volatile读写interleave,也就是不原子,也就是没有用.C++11 supposed会支持 ...

  2. java中的volatile关键字

    java中的volatile关键字 一个变量被声明为volatile类型,表示这个变量可能随时被其他线程改变,所以不能把它cache到线程内存(如寄存器)中. 一般情况下volatile不能代替syn ...

  3. C/C++中的volatile简单描述

    首先引入一篇博客: 1. 为什么用volatile? C/C++ 中的 volatile 关键字和 const 对应,用来修饰变量,通常用于建立语言级别的 memory barrier.这是 BS 在 ...

  4. C和C++中的volatile、内存屏障和CPU缓存一致性协议MESI

    目录 1. 前言2 2. 结论2 3. volatile应用场景3 4. 内存屏障(Memory Barrier)4 5. setjmp和longjmp4 1) 结果1(非优化编译:g++ -g -o ...

  5. [读书笔记]java中的volatile关键词

    以下内容大多来自周志明的<深入理解Java虚拟机>. 当一个变量被volatile修饰后,它将具备两种特性: 1. 保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变 ...

  6. C语言中关键字volatile的含义【转】

    本文转载自:http://m.jb51.net/article/37489.htm 本篇文章是对C语言中关键字volatile的含义进行了详细的分析介绍,需要的朋友参考下 volatile 的意思是“ ...

  7. java多线程中的volatile和synchronized

    package com.chzhao; public class Volatiletest extends Thread { private static int count = 0; public ...

  8. java中的Volatile 变量

    Java 语言中的 volatile 变量可以被看作是一种 “程度较轻的 synchronized”:与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少, ...

  9. 单例模式中的volatile关键字

    在之前学习了单例模式在多线程下的设计,疑惑为何要加volatile关键字.加与不加有什么区别呢?这里我们就来研究一下.单例模式的设计可以参考个人总结的这篇文章   背景:在早期的JVM中,synchr ...

  10. Java中的volatile关键字的功能

    Java中的volatile关键字的功能 volatile是java中的一个类型修饰符.它是被设计用来修饰被不同线程访问和修改的变量.如果不加入volatile,基本上会导致这样的结果:要么无法编写多 ...

随机推荐

  1. sentry 在加载模块时闪退

    这是一个很久之前的问题了,今天记录一下,以便遇到同样问题的同学能够看到此文章 崩溃环境: 目前仅收到 windows 7 的部分用户反馈,在程序启动时发生闪退 问题分析: 查看用户提供的日志,可以看见 ...

  2. websocket 监听端口问题

    有用户反馈使用我们的软件时会出现 ERR_CONNECTION_CLOSED 问题 我们根据反馈排查问题发现是软件的 websocket 监听端口被另一款软件占用了,暂时的解决方法是将占用的软件关闭 ...

  3. Kubernetes leader election 源码分析

    0. 前言 Kubernetes:kube-scheduler 源码分析 介绍了 kube-scheduler 调度 Pod 的逻辑.文中有一点未提的是,在 Kubernetes 集群中,kube-s ...

  4. Redis集群单机环境搭建

    概述 目标:在单台物理机上搭建3主3从共6个节点的Redis集群. 版本:Redis 6.2.4 系统:Ubuntu 18.04 Desktop(IP地址:192.168.100.247) 准备工作 ...

  5. vscode中跑go test时打印详细信息配置

    1.文件>首选项>设置>搜索Test Flags 2.选择在settings.json中编辑 3.添加以下内容 "go.testFlags":[ "-v ...

  6. go最新版本1.15安装配置及编辑器2020.2版本goland

    下载 https://golang.google.cn/dl/ 配置 go env #查看是否安装成功 # 终端输入修改镜像地址 $ go env -w GO111MODULE=on $ go env ...

  7. 【Azure Key Vault】.NET 代码如何访问中国区的Key Vault中的机密信息(Get/Set Secret)

    问题描述 使用 .NET Azure.Identity 中的 DefaultAzureCredential 认证并连接到Azure Key Vault中, 在Key Vault 的示例中,并没有介绍如 ...

  8. set中的erase使用的一个错误

    如果在遍历set的时候去erase很容易出事 事故代码: multiset<int>a; for(auto it=a.begin();it!=a.end();it++){ a.erase( ...

  9. dos-基础用法

    DOS(磁盘操作系统)是一个早期的基于命令行的操作系统,尽管现代操作系统已经发展为图形用户界面(GUI),但是了解和掌握一些基本的DOS命令仍然非常有用,尤其是在处理批处理脚本.网络管理或者在没有图形 ...

  10. stm32 中断处理函数注意事项

    一 前记 最近在公司的一个项目中碰到一个解决了定位很久的 bug , bug 找到的时候发现犯了很低级的错误--在中断处理函数中调用了 printf 函数,因为中断处理函数的调用了不可重入函数,导致接 ...