知识图库
Java知识库
JDK线程池实现原理
Java中的强、软、弱、虚引用
深入拆解Java虚拟机
01 开篇词 | 为什么我们要学习Java虚拟机?
02 Java代码是怎么运行的?
03 Java的基本类型
04 Java虚拟机是如何加载Java类的?
05 JVM是如何执行方法调用的?(上)
06 JVM是如何执行方法调用的?(下)
7 JVM是如何处理异常的?
Java面试常见问题整理
Java面试常见问题-Java 基础篇
Java面试常见问题-Jvm篇
Java面试常见问题-并发篇
Android知识库
Kotlin编程第一课
1 开篇词 | 入门Kotlin有多容易,精通Kotlin就有多难
2 Kotlin基础语法:正式开启学习之旅
3 面向对象:理解Kotlin设计者的良苦用心
4 Kotlin原理:编译器在幕后干了哪些“好事”?
5 实战:构建一个Kotlin版本的四则运算计算器
6 object关键字:你到底有多少种用法?
7 扩展:你的能力边界到底在哪里?
8 高阶函数:为什么说函数是Kotlin的“一等公民”?
9 实战:用Kotlin写一个英语词频统计程序
10 加餐一 | 初识Kotlin函数式编程
11 委托:你为何总是被低估?
12 泛型:逆变or协变,傻傻分不清?
13 注解与反射:进阶必备技能
14 实战:用Kotlin实现一个网络请求框架KtHttp
15 加餐二 | 什么是“表达式思维”?
16 加餐三 | 什么是“不变性思维”?
17 加餐四 | 什么是“空安全思维”?
18 春节刷题计划(一)| 当Kotlin遇上LeetCode
19 春节刷题计划(二)| 一题三解,搞定版本号判断
20 春节刷题计划(三)| 一题双解,搞定求解方程
21 春节刷题计划(四)| 一题三解,搞定分式加减法
22 什么是“协程思维模型”?
23 如何启动协程?
24 挂起函数:Kotlin协程的核心
25 Job:协程也有生命周期吗?
26 Context:万物皆为Context?
27 实战:让KtHttp支持挂起函数
28 期中考试 | 用Kotlin实现图片处理程序
29 题目解答 | 期中考试版本参考实现
30 Channel:为什么说Channel是“热”的?
31 Flow:为什么说Flow是“冷”的?
32 select:到底是在选择什么?
33 并发:协程不需要处理同步吗?
34 异常:try-catch居然会不起作用?坑!
35 实战:让KtHttp支持Flow
36 答疑(一)| Java和Kotlin到底谁好谁坏?
37 集合操作符:你也会“看完就忘”吗?
38 协程源码的地图:如何读源码才不会迷失?
39 图解挂起函数:原来你就是个状态机?
40 加餐五 | 深入理解协程基础元素
41 launch的背后到底发生了什么?
42 Dispatchers是如何工作的?
43 CoroutineScope是如何管理协程的?
44 图解Channel:如何理解它的CSP通信模型?
45 图解Flow:原来你是只纸老虎?
46 Java Android开发者还会有未来吗?
47 Kotlin与Jetpack简直是天生一对!
48 用Kotlin写一个GitHub Trending App
49 结课测试 | “Kotlin编程第一课”100分试卷等你来挑战!
50 结束语 | 不忘初心
Android Framework 教程—基础篇
01 Ubuntu 使用快速入门
02 Make 构建工具入门
03 理解 Unicode UTF-8 UTF-16 UTF-32
04 Linux Shell 脚本编程入门1——核心基础语法
05 SeAndroid 使用极速上手
06 理解 C++ 的 Memory Order
07 AOSP 极速上手
08 系统开发工具推荐
09 添加 Product
运动相关知识
爱上跑步
01 开篇词 | 跑步,不那么简单的事儿
02 跑两步就喘了,是不是我不适合跑步?
03 正确的跑步姿势是什么样的?
04 为什么跑步要先热身?
05 怎样制定你的第一个10公里跑步计划?
06 快跑和慢跑,哪个更燃脂?
07 普通跑步者应该如何选择跑鞋?
08 买跑步装备,不要踩这些坑儿
09 跑步前到底应不应该吃东西?
10 跑步到底伤不伤膝盖?
11 参加了20场马拉松,我是如何准备的?
12 除了马拉松,还能参加哪些跑步赛事?
13 热点问题答疑 :跑完第二天浑身疼,还要不要继续跑?
健身房计划
[DeepSeek]减脂塑形计划
【DeepSeek】训练周期安排
每日餐饮热量控制
减脂期间食物推荐避坑指南
HarmonyOS知识库
其他知识类目
心理学相关
如何学点心理学——关于非专业人士学心理学的一点建议
投射性认同
-
+
首页
06 理解 C++ 的 Memory Order
### 1. 为什么需要 Memory Order 如果不使用任何同步机制(例如 mutex 或 atomic),在多线程中读写同一个变量,那么,程序的结果是难以预料的。主要原因有一下几点: - 简单的读写不是原子操作。 - CPU 可能会调整指令的执行顺序。 - 在 CPU cache 的影响下,一个 CPU 执行了某个指令,不会立即被其它 CPU 看见。 #### 1.1 非原子操作给多线程编程带来的影响 原子操作说的是,一个操作的状态要么就是未执行,要么就是已完成,不会看见中间状态。 下面看一个非原子操作给多线程编程带来的影响: ```c++ int64_t i = 0; // global variable Thread-1: Thread-2: i++; std::cout << i; ``` C++ 并不保证 i++ 是原子操作。从汇编的角度看,读写内存的操作一般分为三步: - 将内存单元读到 cpu 寄存器 - 修改寄存器中的值 - 将寄存器中的值回写入对应的内存单元 进一步,有的 CPU Architecture, 64 位数据(int64_t)在内存和寄存器之间的读写需要两条指令。 这就导致了 i++ 操作在 cpu 的角度是一个多步骤的操作。所以 Thread-2 读到的可能是一个中间状态。 #### 1.2 指令的执行顺序调整给多线程编程带来的影响 为了优化程序的执行性能,编译器和 CPU 可能会调整指令的执行顺序。为阐述这一点,下面的例子中,让我们假设所有操作都是原子操作: ```c++ int x = 0; // global variable int y = 0; // global variable Thread-1: Thread-2: x = 100; while (y != 200) {} y = 200; std::cout << x; ``` 如果 CPU 没有乱序执行指令,那么 Thread-2 将输出 100。然而,对于 Thread-1 来说,x = 100; 和 y = 200; 这两个语句之间没有依赖关系,因此,Thread-1 允许调整语句的执行顺序: ```c++ Thread-1: y = 200; x = 100; ``` 在这种情况下,Thread-2 将输出 0 或 100。 #### 1.3 CPU CACHE 对多线程程序的影响 CPU cache 也会影响到程序的行为。下面的例子中,假设从时间上来讲,A 操作先于 B 操作发生: ```c++ int x = 0; // global variable Thread-1: Thread-2: x = 100; // A std::cout << x; // B ``` 尽管从时间上来讲,A 先于 B,但 CPU cache 的影响下,Thread-2 不能保证立即看到 A 操作的结果,所以 Thread-2 可能输出 0 或 100。 ### 2. 同步机制 对于 C++ 程序来说,解决以上问题的办法就是使用同步机制,最常见的同步机制就是` std::mutex` 和 `std::atomic`。从性能角度看,通常使用 `std::atomic` 会获得更好的性能。 C++ 提供了四种 memory ordering : - Relaxed ordering - Release-Acquire ordering - Release-Consume ordering - Sequentially-consistent ordering #### 2.1 Relaxed ordering 在这种模型下,std::atomic 的 load() 和 store() 都要带上 memory_order_relaxed 参数。Relaxed ordering 仅仅保证 load() 和 store() 是原子操作,除此之外,不提供任何跨线程的同步。 先看看一个简单的例子: ```c++ std::atomic<int> x = 0; // global variable std::atomic<int> y = 0; // global variable Thread-1: Thread-2: //A // C r1 = y.load(memory_order_relaxed); r2 = x.load(memory_order_relaxed); //B // D x.store(r1, memory_order_relaxed); y.store(42, memory_order_relaxed); ``` 执行完上面的程序,可能出现 r1 = = r2 = = 42。理解这一点并不难,因为编译器允许调整 C 和 D 的执行顺序。如果程序的执行顺序是 D -> A -> B -> C,那么就会出现 r1 = = r2 = = 42。 如果某个操作只要求是原子操作,除此之外,不需要其它同步的保障,就可以使用 Relaxed ordering。程序计数器是一种典型的应用场景: ```c++ #include <cassert> #include <vector> #include <iostream> #include <thread> #include <atomic> std::atomic<int> cnt = {0}; void f() { for (int n = 0; n < 1000; ++n) { cnt.fetch_add(1, std::memory_order_relaxed); } } int main() { std::vector<std::thread> v; for (int n = 0; n < 10; ++n) { v.emplace_back(f); } for (auto& t : v) { t.join(); } assert(cnt == 10000); // never failed return 0; } ``` #### 2.2 Release-Acquire ordering 在这种模型下,store() 使用 `memory_order_release`,而 load() 使用 `memory_order_acquire`。这种模型有两种效果,第一种是可以限制 CPU 指令的重排: - 在 store() 之前的所有读写操作,不允许被移动到这个 store() 的后面。 - 在 load() 之后的所有读写操作,不允许被移动到这个 load() 的前面。 除此之外,还有另一种效果:假设 Thread-1 store() 的那个值,成功被 Thread-2 load() 到了,那么 Thread-1 在 store() 之前对内存的所有写入操作,此时对 Thread-2 来说,都是可见的。 下面的例子阐述了这种模型的原理: ```c++ #include <thread> #include <atomic> #include <cassert> #include <string> std::atomic<bool> ready{ false }; int data = 0; void producer() { data = 100; // A ready.store(true, std::memory_order_release); // B } void consumer() { while (!ready.load(std::memory_order_acquire)){} // C assert(data == 100); // never failed // D } int main() { std::thread t1(producer); std::thread t2(consumer); t1.join(); t2.join(); return 0; } ``` 让我们分析一下这个过程: - 首先 A 不允许被移动到 B 的后面。 - 同样 D 也不允许被移动到 C 的前面。 - 当 C 从 while 循环中退出了,说明 C 读取到了 B store() 的那个值,此时,Thread-2 保证能够看见 Thread-1 执行 B 之前的所有写入操作(也即是 A)。 #### 2.3 Release-Consume ordering 在这种模型下,`store()` 使用 `memory_order_release`,而 `load()` 使用 `memory_order_consume`。这种模型有两种效果,第一种是可以限制 CPU 指令的重排: - 在 `store()` 之前的与原子变量相关的所有读写操作,不允许被移动到这个 `store() `的后面。 - 在 `load()` 之后的与原子变量相关的所有读写操作,不允许被移动到这个 `load() `的前面。 除此之外,还有另一种效果:假设 Thread-1 store() 的那个值,成功被 Thread-2 load() 到了,那么 Thread-1 在 store() 之前对与原子变量相关的内存的所有写入操作,此时对 Thread-2 来说,都是可见的。 下面的例子阐述了这种模型的原理: ```c++ #include <thread> #include <atomic> #include <cassert> #include <string> std::atomic<std::string*> ptr; int data; void producer() { std::string* p = new std::string("Hello"); //A data = 42; //ptr依赖于p ptr.store(p, std::memory_order_release); //B } void consumer() { std::string* p2; while (!(p2 = ptr.load(std::memory_order_consume))) //C ; // never fires: *p2 carries dependency from ptr assert(*p2 == "Hello"); //D // may or may not fire: data does not carry dependency from ptr assert(data == 42); } int main() { std::thread t1(producer); std::thread t2(consumer); t1.join(); t2.join(); } ``` 让我们分析一下这个过程: - 首先 A 不允许被移动到 B 的后面。 - 同样 D 也不允许被移动到 C 的前面。 - data 与 ptr 无关,不会限制他的重排序 - 当 C 从 while 循环中退出了,说明 C 读取到了 B store() 的那个值,此时,Thread-2 保证能够看见 Thread-1 执行 B 之前的与原子变量相关的所有写入操作(也即是 A)。 #### 2.4 Sequentially-consistent ordering `Sequentially-consistent ordering` 是缺省设置,在 `Release-Acquire ordering` 限制的基础上,保证了所有设置了 `memory_order_seq_cst` 标志的原子操作按照代码的先后顺序执行。 ### 参考资料 - [理解 C++ 的 Memory Order](https://link.juejin.cn/?target=http%3A%2F%2Fsenlinzhan.github.io%2F2017%2F12%2F04%2Fcpp-memory-order%2F) - 《C++新经典》 第十七章 - 《C++并发编程实战(第2版)》第五章 - [如何理解 C++11 的六种 memory order?](https://link.juejin.cn/?target=https%3A%2F%2Fwww.zhihu.com%2Fquestion%2F24301047) - [现代C++的内存模型](https://link.juejin.cn/?target=https%3A%2F%2Fzhuanlan.zhihu.com%2Fp%2F382372072) - [C++ atomics and memory ordering](https://link.juejin.cn/?target=https%3A%2F%2Fbartoszmilewski.com%2F2008%2F12%2F01%2Fc-atomics-and-memory-ordering%2F) - [Understanding Atomics and Memory Ordering](https://link.juejin.cn/?target=https%3A%2F%2Fdev.to%2Fkprotty%2Funderstanding-atomics-and-memory-ordering-2mom) - [atomic Weapons: The C++ Memory Model and Modern Hardware](https://link.juejin.cn/?target=https%3A%2F%2Fherbsutter.com%2F2013%2F02%2F11%2Fatomic-weapons-the-c-memory-model-and-modern-hardware%2F) - [C++11新特性内存模型总结详解--一篇秒懂](https://link.juejin.cn/?target=https%3A%2F%2Fwww.cnblogs.com%2Fbclshuai%2Fp%2F15898116.html) - [C++ Memory_order的理解](https://link.juejin.cn/?target=https%3A%2F%2Fblog.csdn.net%2Fbaidu_20351223%2Farticle%2Fdetails%2F115765606) - [memory_order](https://link.juejin.cn/?target=https%3A%2F%2Fen.cppreference.com%2Fw%2Fcpp%2Fatomic%2Fmemory_order%23Release-Acquire_ordering)
嘿手大叔
2025年1月3日 10:53
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码