当前位置: 首页 > news >正文

理解原子变量之二:从volatile到内存序-进一步的认识

目录

实例1

实例2

实例3

内存序中两个最重要的概念

补记

结论


实例1

看下面的例子:在vs2013中建立如下工程:

#include <thread>
#include <iostream>
#include <chrono>bool done = false;void worker(){std::this_thread::sleep_for(std::chrono::seconds(3));done = true;
}int main(){std::thread workerThread(worker);while (!done){//std::cout << "waiting" << std::endl;不知为什么,加上这句打印就可以跳出循环}std::cout << "thread finished" << std::endl;workerThread.join();std::cin.get();return 0;
}

使用release模式编译该工程,且优化采用O2

结果是程序卡住了,本来3秒钟就可以跳出while(!done)死循环,现在一直停着。

vs2013 release 模式下多线程卡住演示

实例2

实例1为什么会卡住呢?因为编译器在打开优化的情况下,“武断”的认为done永远都是false。编译器意识不到子线程也在改变done的取值,所以就认为done是常量,而不会在运行到while()时读取done的实际值,于是while(!done)就变成了while(true)。假如采用debug模式编译,编译器不会“自作聪明”,程序在运行时将老老实实的不断读取done的实时值,直到done==true为止。所以,debug模式不会卡住,而release模式会卡住(除非在编译时禁止优化)。

现在稍微修改一下代码,用volatile修饰 bool done。即使在release下编译,程序仍然正常走完,不卡住:

这是因为,volatile在提示编译器,done是一个不能被优化掉的变量。即使在release模式下,每次走到while时,也要查看done的值。

回到原子变量的话题上来,再看第三个例子:

实例3

现在用atomic<bool>代替volatile bool。

#include <thread>
#include <iostream>
#include <chrono>
#include <atomic>std::atomic<bool> done(false);void worker() {std::this_thread::sleep_for(std::chrono::seconds(3));done.store(true);
}int main() {std::thread workerThread(worker);while (!done.load()) {//std::cout << "waiting" << std::endl;不知为什么,加上这句打印就可以跳出循环}std::cout << "thread finished" << std::endl;workerThread.join();std::cin.get();return 0;
}

结果与使用volatile一样,while(!done.load())不会一直阻塞程序向下运行:

 看到了上面的三个例子,接下来就要请出原子变量里面一个非常重要的概念:内存序。

内存序中两个最重要的概念

std::memory_order - cppreference.com列出了C++11标准里的六个内存序(memory_order)。这6个内存序的作用将在下一篇文章描述。其中多次出现一个词汇“可见”(visible)

以MEMORY_ORDER_ACQUIRE为例,其中文解释是:

有此内存定序的加载操作,在其影响的内存位置进行获得操作:当前线程中读或写不能被重排到此加载之前。其他线程的所有释放同一原子变量的写入,能为当前线程所见

 “其他线程的所有释放同一原子变量的写入,能为当前线程所见”这句话的意思是什么?我们只要理解了什么是不可见,就理解了可见。对照实例1,在不使用volatile的情况下,子线程workerThread对done的修改,没有影响到主线程的while(!done)逻辑,这就是说,子线程对done的写入,不为主线程可见。

反观实例2和3,子线程修改了done,主线程的运行随之受了影响,这就是可见。

内存序里有两个重要的概念,一是指令排序,在下一篇里面将介绍;另一个就是可见性(visibility)。从例子2和3可见,在可见性这方面,原子变量的作用与volatile是一样的:原子变量被一个线程改变后,另一个线程肯定会发现这个变化

补记

实例1,2,3都是在vs2013下演示的。在vs2019下,实例1效果有区别:vs2019的while循环在示例1中同样不会阻塞。这是因为,不同的编译器遵循不同的c++标准。即使遵循相同的C++标准,不同的编译器对同一个标准的贯彻程度也不一样:有的编译器严格遵循标准,既不多做,也不少做;有的编译器在某些次要方面可能达不到C++标准的要求(少做);还有的编译器为了安全性等方面考虑,甚至超过了C++标准的要求(多做)。vs2019很可能属于超过标准这一种:即使是多线程编程中使用普通的非原子变量,非易失(volatile)变量,仍能保证可见性(个人猜测)。

但无论如何,本文探讨的是C++11标准的内存序,不针对具体编译器。在遵守C++11标准方面,VS2013似乎比2019更贴切,所以三个实例均采用2013示范。

结论

前文解释了原子变量的第一个功能:它相当于粒度很细的互斥锁。本文介绍了原子变量的第二个功能:可见性。原子变量保证了多线程编程中的可见性。事实上,原子变量共负担了三个功能。下一篇文章将介绍其第三个功能--限制指令重排。

http://www.lryc.cn/news/475502.html

相关文章:

  • DICOM标准:MR图像模块属性详解——磁共振成像(MR)在DICOM中的应用
  • Linux内核与用户空间
  • 计算机网络-以太网小结
  • 找树根和孩子c++
  • 植物源UDP-糖基转移酶及其分子改造-文献精读75
  • Redis中String 的底层实现是什么?
  • 像mysql一样查询es
  • SpringBoot中@Validated或@Valid注解校验的使用
  • HashMap为什么线程不安全?
  • 类加载器及反射
  • aws boto3 下载文件
  • 3DDFA-V3——基于人脸分割几何信息指导下的三维人脸重建
  • 求串长(不使用任何字符串库函数)
  • 第02章 MySQL环境搭建
  • linux系统编程 man查看manual.stat
  • 从网络到缓存:在Android中高效管理图片加载
  • 【数据结构】链表详解:数据节点的链接原理
  • 使用AWS Redshift从AWS MSK中读取数据
  • 从0开始学统计-数据类别与测量层次
  • 使用AIM对SAP PO核心指标的自动化巡检监控
  • C++——unordered_map和unordered_set的封装
  • 微信小程序scroll-view吸顶css样式化表格的表头及iOS上下滑动表头的颜色覆盖、z-index应用及性能分析
  • 【高中数学】数列
  • 数字媒体技术基础:AMF(ACES 元数据文件 )
  • Apache Dubbo (RPC框架)
  • LeetCode 3226. 使两个整数相等的位更改次数
  • 面试经典 150 题:189、383
  • Python模拟真人动态生成鼠标滑动路径
  • 如何压缩pdf文件的大小?5分钟压缩pdf的方法推荐
  • 【SQL】[2BP01] ERROR: cannot drop table course because other objects depend on it