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. ElementUI导入Excel文件

    功能介绍 最近用ElementUI做管理系统需要把excel数据导入到系统内,我想这是一个很常见的功能点,把它分享出来,希望对大家有所帮助:) 实现效果 实现步骤 1.定义导入组件 <el-up ...

  2. Linux上安装和部署git

    本机环境: [git@rhel-server .ssh]$ cat /proc/version Linux version 2.6.32-358.el6.x86_64 1.安装 yum install ...

  3. golang常用库包:redis操作库go-redis使用(01)-Redis数据类型简介和连接Redis的几种方式

    第一篇:go-redis使用,介绍Redis基本数据结构和其他特性,以及 go-redis 连接到Redis(本篇) https://www.cnblogs.com/jiujuan/p/1720716 ...

  4. 【Azure Redis】中国区Redis在东三区的资源无法在通过门户上与北三区资源之间建立灾备链接

    问题描述 为应用启用灾备管理,在北三区建立了一个Azure Redis,同时,在东三区也建立了一个同样的Prem级Redis服务.但是在建立灾备(DR:Disease Recovery)时候,却无法选 ...

  5. 【Azure API 管理】APIM不能连接到 App Service (APIM cannot connect to APP service)

    问题描述 APIM 无法正确连接到App Service,返回500错误: { "statusCode": 500, "message": "Inte ...

  6. 【Azure 应用服务】App Services 恶意软件防护相关

    问题描述 App Services 恶意软件防护相关资料,App Service是否默认开启病毒防护呢? 问题解答 App Services 默认启用了Antimalware 软件功能,Microso ...

  7. 图查询语言 nGQL 简明教程 vol.01 快速入门

    本文旨在让新手快速了解 nGQL,掌握方向,之后可以脚踩在地上借助文档写出任何心中的 NebulaGraph 图查询. 视频 本教程的视频版在B站这里. 准备工作 在正式开始 nGQL 实操之前,记得 ...

  8. 在.NET程序中整合微软的Playwright,使用 Playwright 的最佳实践和技巧

    Playwright 是一个由 Microsoft 开发的开源工具,用于自动化 Web 浏览器的测试和操作.它提供了一种跨浏览器.跨平台的自动化解决方案,可以在 Chromium.Firefox 和 ...

  9. 了解一下IOC和AOP

    简单学习一下IOC和AOP 聊一聊 IOC& AOP之前,先解释几个问题: AOP的老大哥OOP和老老大哥POP 什么是IoC? IoC 解决了什么问题? 什么是 AOP? AOP 解决了什么 ...

  10. C++ STL之 map 学习笔记

    •何为 map? map 是 STL 的一个关联容器,它提供一对一的数据处理,map 中存放的是一个 key-value键值对,其类型可以自己定义: 第一个可以称为关键字,每个关键字在 map 中只能 ...