理解 C++ 的 Memory Order 以及 atomic 与并发程序的关系
为什么需要 Memory Order
如果不使用任何同步机制(例如 mutex 或 atomic),在多线程中读写同一个变量,那么,程序的结果是难以预料的。简单来说,编译器以及 CPU 的一些行为,会影响到程序的执行结果:
- 即使是简单的语句,C++ 也不保证是原子操作。
- CPU 可能会调整指令的执行顺序。
- 在 CPU cache 的影响下,一个 CPU 执行了某个指令,不会立即被其它 CPU 看见。
原子操作说的是,一个操作的状态要么就是未执行,要么就是已完成,不会看见中间状态。例如,在 C++11 中,下面程序的结果是未定义的:
int64_t i = 0; // global variableThread-1: Thread-2:i = 100; std::cout << i;
C++ 并不保证i = 100
是原子操作,因为在某些 CPU Architecture 中,写入int64_t
需要两个 CPU 指令,所以 Thread-2 可能会读取到i
在赋值过程的中间状态。
另一方面,为了优化程序的执行性能,CPU 可能会调整指令的执行顺序。为阐述这一点,下面的例子中,让我们假设所有操作都是原子操作:
int x = 0; // global variableint y = 0; // global variableThread-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 允许调整语句的执行顺序:
Thread-1:y = 200;x = 100;
在这种情况下,Thread-2 将输出0
或100
。
CPU cache 也会影响到程序的行为。下面的例子中,假设从时间上来讲,A 操作先于 B 操作发生:
int x = 0; // global variableThread-1: Thread-2:x = 100; // A std::cout << x; // B
尽管从时间上来讲,A 先于 B,但 CPU cache 的影响下,Thread-2 不能保证立即看到 A 操作的结果,所以 Thread-2 可能输出0
或100
。
从上面的三个例子可以看到,多线程读写同一变量需要使用同步机制,最常见的同步机制就是std::mutex
和std::atomic
。然而,从性能角度看,通常使用std::atomic
会获得更好的性能。
C++11 为std::atomic
提供了 4 种 memory ordering:
- Relaxed ordering
- Release-Acquire ordering
- Release-Consume ordering
- Sequentially-consistent ordering
默认情况下,std::atomic
使用的是 Sequentially-consistent ordering。但在某些场景下,合理使用其它三种 ordering,可以让编译器优化生成的代码,从而提高性能。
Relaxed ordering
在这种模型下,std::atomic
的load()
和store()
都要带上memory_order_relaxed
参数。Relaxed ordering 仅仅保证load()
和store()
是原子操作,除此之外,不提供任何跨线程的同步。
先看看一个简单的例子:
1 2 3 4 5 6 |
std::atomic<int> x = 0; // global variable std::atomic<int> y = 0; // global variable Thread-1: Thread-2: r1 = y.load(memory_order_relaxed); // A r2 = x.load(memory_order_relaxed); // C x.store(r1, memory_order_relaxed); // B y.store(42, memory_order_relaxed); // D |
执行完上面的程序,可能出现r1 == r2 == 42
。理解这一点并不难,因为编译器允许调整 C 和 D 的执行顺序。如果程序的执行顺序是 D -> A -> B -> C,那么就会出现r1 == r2 == 42
。
如果某个操作只要求是原子操作,除此之外,不需要其它同步的保障,就可以使用 Relaxed ordering。程序计数器是一种典型的应用场景:
#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 failedreturn 0;
}
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 来说,都是可见的。
下面的例子阐述了这种模型的原理:
#include <thread>
#include <atomic>
#include <cassert>
#include <string>
std::atomic<bool> ready{ false };
int data = 0;
void producer()
{data = 100; // Aready.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)。
如下例子展示了在三个线程之间进行 release-acquire 内存序的同步机制实例:
- 其中对于读取修改在写入的操作,可以使用 memory_order_acq_rel 标志位,如下所示。
#include <thread>
#include <atomic>
#include <cassert>
#include <vector>std::vector<int> data;
std::atomic<int> flag = {0};void thread_1()
{data.push_back(42);flag.store(1, std::memory_order_release);
}void thread_2()
{int expected=1;while (!flag.compare_exchange_strong(expected, 2, std::memory_order_acq_rel)) {expected = 1;}
}void thread_3()
{while (flag.load(std::memory_order_acquire) < 2);assert(data.at(0) == 42); // will never fire
}int main()
{std::thread a(thread_1);std::thread b(thread_2);std::thread c(thread_3);a.join(); b.join(); c.join();
}
Release-Consume ordering
比起 Release-Acquire 模型较为内存限制较弱,仅对 load 该原子变量具有依赖的相关变量有效。例子如下所示:
#include <thread>
#include <atomic>
#include <cassert>
#include <string>std::atomic<std::string*> ptr;
int data;void producer()
{std::string* p = new std::string("Hello");data = 42;ptr.store(p, std::memory_order_release);
}void consumer()
{std::string* p2;while (!(p2 = ptr.load(std::memory_order_consume)));assert(*p2 == "Hello"); // never fires: *p2 carries dependency from ptrassert(data == 42); // may or may not fire: data does not carry dependency from ptr
}int main()
{std::thread t1(producer);std::thread t2(consumer);t1.join(); t2.join();
}
与 volatile 关键字的区别
std::atomic
用于无锁条件下,实现多线程间的数据访问,是编写并发程序的有效工具之一volatile
用于读写内存时禁止编译器进行内存优化,是用于专有内存的有效工具之一
参考资料
- C++ atomics and memory ordering
- cppreference.com - std::memory_order
- Atomic Usage examples
- C++11 introduced a standardized memory model. What does it mean?
- bRPC - Memory fence
- Acquire and Release Semantics
理解 C++ 的 Memory Order 以及 atomic 与并发程序的关系相关推荐
- atomic 内存序_如何理解 C++11 的六种 memory order?
GTHub:高并发编程--多处理器编程中的一致性问题(下)zhuanlan.zhihu.com 4 C++ Memory model 4.0 写在前面 C++ memory order是对atomi ...
- 深入理解Memory Order
深入理解Memory Order cpu 保证 cache 编程技术 lock-free wait-free Read–modify–write Compare-And-Swap(CAS) cas原理 ...
- 深入理解Java虚拟机-如何利用VisualVM对高并发项目进行性能分析
Java虚拟机深入理解系列全部文章更新中- 深入理解Java虚拟机-Java内存区域透彻分析 深入理解Java虚拟机-常用vm参数分析 深入理解Java虚拟机-JVM内存分配与回收策略原理,从此告别J ...
- Java IO的原理 入门理解,input和output和java程序的关系
Java IO的原理 入门理解,input和output和java程序的关系 1.Java IO的原理 2.input和output的理解 3.IO流的分类 4.IO流体系(蓝色为重点.常用) 5.. ...
- 【并发2】理解并发程序的执行
这是 bilibili-[完结] 2020 南京大学 "操作系统:设计与实现" (蒋炎岩) 的课程笔记 本讲内容: - 串行程序的状态机模型 - 状态机模型的应用 - 并发程序的状 ...
- [译]C++中的内存同步模式(memory order)
C++11 引入了一个有些晦涩的主题: 内存模型,不过一般都只会在需要 Lock-Free 编程时才会遇到,这里翻译一篇相关文章,希望能够给有兴趣的朋友多些参考.原文在这里. 内存模型中的同步模式(m ...
- ARMV8 datasheet学习笔记3:AArch64应用级体系结构之Memory order
1.前言 2.基本概念 Observer 可以发起对memory read/write访问的都是observer; Observability 是一种观察能力,通过read可以感知到别的observe ...
- 为什么写了value属性 jq赋值value值不显示_[Go基础]理解 Go 标准库中的 atomic.Value 类型
转载声明 文章作者:喵叔 上次更新:2019-03-15 许可协议:CC BY-NC-ND 4.0(转载请注明出处) 原文链接:https://blog.betacat.io/post/golang- ...
- 深入理解 RPC : 基于 Python 自建分布式高并发 RPC 服务
RPC(Remote Procedure Call)服务,也即远程过程调用,在互联网企业技术架构中占据了举足轻重的地位,尤其在当下微服务化逐步成为大中型分布式系统架构的主流背景下,RPC 更扮演了重要 ...
最新文章
- 《Adobe Fireworks CS6中文版经典教程》——1.5使用多个文档
- 精通python网络爬虫-精通Python网络爬虫:核心技术、框架与项目实战 PDF
- 当微信小程序遇上TensorFlow:Server端实现补充
- Happy New Year
- mybitas oracle.sql.clob,Oracle使用简单函数
- hdu2133: What day is it
- 计算机视觉与深度学习 | Matlab实现旋转矩阵R到四元数的转换(源代码)
- Sql Server 按格式输出日期
- 第七章 PX4-Mavlink解析
- rails jquery_Spring与Rails的jQuery UJS
- 监听手指是否离开屏幕android_Flutter事件监听
- virtualbox安装android6.0并设置分辨率为1920x1080x32
- JavaScript Try Catch:异常处理说明
- 不止是安防 红外摄像机在应急产业的应用
- java 注释标记_如何标记,像老板一样注释内容
- 【java笔记】Object类
- java 哈希表和向量_Java基础知识笔记(一:修饰词、向量、哈希表)
- Thingworx入门学习
- js去除字符串空格(空白符)
- obs源码分析【四】:obs录制的窗口截图与视频编码
热门文章
- 中文字串截取无乱码的问题
- 多個不同格式文件如何合並至一個PDF檔
- android来电事件,android – 来电时没有响铃事件
- java获取对象的子_java – 如何根据子对象字段获取父对象
- 计算机病毒属于什么类工具,什么是计算机病毒?有哪些类型
- js与c语言互相调用,Objc与JS间相互调用
- 详解TCP协议三次握手四次挥手
- mysql怎么插入10w测试数据_mysql快速插入100万测试数据
- mysql 从库 问题_一篇文章帮你解决Mysql 中主从库不同步的问题
- android struts2 图片上传,xhEditor struts2实现图片上传