再说 c++11 内存模型
可见性与乱序
在说到内存模型相关的东西时,我们常常会说到两个名词:乱序与可见性,且两者经常交错着使用,容易给人错觉仿佛是两个不同的东西,其实不是这样,他们只是从不同的角度来描述一个事情,本质是相同的。比如说,我们有如下代码:
atomic<int> g_payLoad = {0};
atomic<int> g_guard = {0};
// thread 0
void foo1()
{
g_payLoad.store(42, memory_order_relaxed);
g_guard.store(1, memory_order_relaxed);
}
// thread 1
void foo2()
{
int r = g_guard.load(memory_order_relaxed);
if (r)
{
r = g_payLoad.load(memory_order_relaxed);
}
}
因为 g_guard 与 g_payLoad 的读写都是以 relaxed 方式进行,因此我们会认为 foo2() 中 g_guard.load() 与 g_payLoad.load() 有可能会乱序(从不同的地址 load, cpu 可能会 speculate & prefetch,etc),但从另一个角度来看,g_payLoad 被 reorder 到 g_guard 之前(其它类型乱序同理),其实就相当于 g_payLoad 在 foo1 中的修改没有被 foo2 所看到,同一件事情的两个不同角度,如此而已。
但在处理 c++11 的内存模型时,语言标准上的措词却基本上是基于“可见性”来描述的(和 Java 的内存模型描述基本一致),看得多了,我也渐渐发现从“可见性”来理解问题有时真会容易很多,不信请继续看。
Happen-before
Happen-before 指的是程序指令间的一种关系,且是一种运行时的关系,是动态的,如果指令 A happen-before B, 则 A 的副作用能够被 B 所看到,容易理解对吧?且暂时可简单认为 happen-before 具有传递性(transitive)(准确地说是在不考虑 consume ordering的情况下), 如果 A happen-before B, B happen-before C,则我们可以认为 A happen-before C。
两条指令 A 与 B 如要满足 happen-before 关系,必须满足如下条件之一:
- A sequence-before B: 这个指的是在同一个线程里两条指令的关系,从程序源码上看如果指令 A 在 B 之前,则 A sequence before B, 比如前面代码中 g_payLoad.store() 这一行代码就是 sequence-before g_guard.store().
- A synchronize-before B: 这个指的是两个线程间的指令的关系,接下来一节再介绍。
因此当我们在理解代码时,如果想弄清楚当前线程里某些代码是否能看到公共变量在别的线程里被修改后的效果,我们应该第一时间这样来考虑,当前使用这些公共变量的代码能与别的线程里修改这些公共变量的代码建立一个 happen-before 的关系吗?如果有 happen-before 关系,则可以看到,如果没有则不一定,还是使用前面的例子:
// thread 0
g_payLoad.store(42, memory_order_relaxed); // 1
g_guard.store(1, memory_order_relaxed); // 2
// thread 1
int r = g_guard.load(memory_order_relaxed); // 3
if (r) r = g_payLoad.load(memory_order_relaxed); // 4
如果 g_payLoad.load() 要 load 到 #1 中所写入的值,则 #1 和 #4 这两者之间必须要有 happen-before 关系,而他们之间要有 happen before 关系,只需满足如下两个条件之一:
- 它们真的有 happen-before 关系,即上述代码中, #1 happen-before #4, 不过这个没法只根据 1 和 4 这两行代码来证明, 原因下面一节会具体解释。
- g_guard.store() 与 g_guard.load() 之间有 happen-before 关系, 即 #2 和 #3 之间有 happen-before 关系,则根据传递性,我们可以知道 #1 happen-before #4.
Synchronize-with
Sychronize-with 指的是两个线程间的指令在运行时的一种特殊关系,这种关系是通过 acquire/release 语义来达成的,具体来说,如果:
- 线程 A 中对某个变量 m 以 release 的方式进行修改,m.store(42, memory_order_release).
- 线程 B 中对变量 m 以 acquire 的方式进行读取,并且读到线程 A 所写的值(或以 A 为开始的一个 release sequence 所写入的值),则线程 B 中读 m 变量的那条指令与线程 A 中修改 m 的指令存在 synchronize-with 的关系。
顺便说一下 release sequence,操作 A 对变量 m 的 release sequence 指的是满足如下两个条件且作用于 m 上的操作的序列:
- 该操作与 A 在同一个线程内,且跟在 A 操作之后。
- 该操作在其它线程内,但是是以 Read-Modify-Write(RMW) 的方式对 m 进行修改。
其中, A 操作以 store release 的方式修改变量 m,而 RMW 操作是一种很特殊的操作,它的 read 部分要求永远能读到该变量最新的值。[参看 c++ 标准 29.3.12]
下图展示了一个经典的 synchonize-with 的例子:
这里有一点需要明确,操作 A 与 操作 B 是否存在 synchronize-with 关系,关键在于 B 是否读到了 A 所写入的内容(或 release sequence 写入的内容), 而 B 是否能读到 A 写入的内容与 A 这个操作是否已经进行了无关,c++ 标准的规定是,假如读到了,则 A synchronize-with B, 否则则不,因此对于某些关键变量,如果你想保证当你去读它时,总能读到它在别的线程里写入的最新的值,一般来说,你需要额外再设置一个 flag 用于进行同步,一个标准的模式就是前面例子中的 g_payLoad 与 g_guard。 g_payLoad 是你关注的关键信息或者说想要发布到别的线程的关键信息,而 g_guard 则是一个 flag,用于进行同步或者说建立 happen-before 的关系,只有建立了 happen-before 关系,你去读 g_payLoad 时,才能保证读到最新的内容。
sequential consistency
sequential consistency 这种模型实在是太美好了,它让编码变得这样地简单直接,一切都是和谐有序的,社会主义般地美好,而这种美好又是那么地触手可及,只要你完全不要使用其它模型,SC 就是你的了!而你所需付出的代价只是在某些平台上一点点效率的损失,就那么一点点!但不幸 c++ 程序员里面处女座的太多,因此我们得处理 acquire/release,甚至是 relaxed。而当 SC 与其它模型混合在了一起时,一定不要想当然以为有 SC 出现的地方就都是曾经的美好乐园,不一定了。
- 以 sequential consistency 方式进行的 load() 操作含有 acquire 语义。
- 以 sequential consistency 方式进行的 store() 操作含有 release 语义。
- 所有 sequential consistency 操作在全局范围内有一个一致的顺序,但这个顺序与 happen-before/synchronize-with 没有直接联系,sequential consistency Load 不一定会 Load() 到最新的值,sequential consistency write() 也并不一定就能马上被其它非 sequential consistency load() 所能 load() 到。
除此,需要注意的是 sequential consistency 类型的 fence,它是个例外,和纯粹的 SC load 和 SC store 不同,SC fence 能建立“类似” happen-before 的关系,参看 c++ 标准 29.3.6:
假如存在两个作用于变量 m 的操作 A 和操作 B,A 修改 m,而 B 读取 m,如果存在两个 memory_order_seq_cst 类型的 fence X 与 Y,使得:
1. A sequence-before X,且 Y sequence-before B.
2. 且 X 在全局顺序上处于 Y 之前(因为 X 和 Y 是 memory_order_seq_cst 类型的,因此肯定有一个全局顺序)。
则 B 会读到 A 写入的数据。
Dekker and Petterson's Algo
现在让我们尝试用前面介绍的知识来解决两个问题,如下是一个简化版的 Dekker's Algo,假设所有数据的初始值都是 0,则显然,如果所有内存操作都是以 relaxed 方式进行的话,则 r1 == r2 == 0 是可能的,因为 thread 0 里对 g_a 的读取不一定能看到 thread 1 对 g_a 的修改,对 g_b 的读取同理,现在的问题是,怎么才能阻止同时读到 r1 == r2 == 0?
// thread 0
g_a.store(42, memory_order_relaxed); // 1
r1 = g_b.load(memory_order_relaxed); // 2
// thread 1
g_b.store(24, memory_order_relaxed); // 3
r2 = g_a.load(memory_order_relaxed); // 4.
直接机械地套用 acquire/release 是不行的,#1 和 #4 不一定能建立 synchronize-with 关系,且 g_a 本身是关键变量,我们需要保证的是能读到它的最新值,直接用它来建立 synchonize-with 显然不能保证这点,#3 和 #2 同理。一个解法是分别在两个 thread 里分别加入一个 SC fence:
// thread 0
g_a.store(42, memory_order_relaxed);
atomic_thread_fence(memory_order_seq_cst); // fence 1
r1 = g_b.load(memory_order_relaxed);
// thread 1
g_b.store(24, memory_order_relaxed);
atomic_thread_fence(memory_order_seq_cst); // fence 2
r2 = g_a.load(memory_order_relaxed); // 4
原理很简单,因为 fence 1 和 fence 2 是 sequential consistency 类型的, 因此它们的副作用在全局上有一个固定顺序,要么 fence 1 先于 fence 2,要么 fence 2 先于 fence 1,根据前一节的介绍,我们知道要么 g_a 读到 42, 要么 g_b 读到 24, 因此肯定不会出现 r1 == r2 == 0.
现在是第二个问题,如下是 Peterson's Algo 的简化写法, 用于实现互斥锁,问题的关键是怎么保证 flag0 在线程 1 里能读到线程 0 对它的修改?等价问题是怎么阻止 #3 被 reorder 到 #1 之前,#6 被 reorder 到 #4 之前?
// Thread 0
flag0.store(true, memory_order_relaxed); // 1
r0 = turn.store(0, memory_order_relaxed); // 2
r1 = flag1.load(memory_order_relaxed); // 3
// Thread 1
flag1.store(true, memory_order_relaxed); // 4
r0 = turn.exchange(1, memory_order_relaxed); // 5
r1 = flag0.load(memory_order_relaxed); // 6
现在我们尝试用 acquire/release 语义来解决它,假设 thread 0 先执行并进入了临界区,然后 thread 1 后执行,当 thread 1 执行到 #6 时,怎么保证能看到 thread 0 对 flag0 的修改呢?根据前面第二节的介绍,我们知道关键在于要保证 #1 happen-before #6,又由于 #1 和 #6 分别在不同的线程,因此其实就是要保证 #1 synchronize-with #6,因此我们需要在 thread 0 中以 release 的方式写一个变量 A,然后在 thread 1 中以 acquire 的方式读取该变量 A,那么我们应该选取哪个变量作为这个关键变量呢?
- flag0 不行,原因与前面第一节的例子相同,flag0 是我们要读取的关键变量,我们要保证的是能读取到它的最新值,而不是通过它来实现 synchronize-with.
- flag1 也不行,flag1 在 thread 0 只有一个 load 操作,没有 release 语义(但如果用 flag1.fetech_add(0, memory_order_acq_rel) 呢?应该也是行的,只是不是最好,多了一次无谓的写操作)。
- 最优选择应该是 turn 变量。
因此得到如下解法如下:
// Thread 0
flag0.store(true, memory_order_relaxed); // 1
r0 = turn.exchange(1, memory_order_acq_rel); // 2
r1 = flag1.load(memory_order_acquire); // 3
// Thread 1
flag1.store(true, memory_order_relaxed); // 4
r0 = turn.exchange(2, memory_order_acq_rel); // 5
r1 = flag0.load(memory_order_acquire); // 6
让我啰嗦点指出其中一个关键,#2 和 #5 能建立 synchronize-with 关系的关键在于 exchange 是一个 RMW 操作,它的读操作总能够读到变量最新的值(c++ 标准 29.3.12),因此当 thread 0 先执行时,turn 会被以 release 的方式写入一个值,再然后后面 thread 1 执行 #3 ,会以 acquire 的方式对 turn 进行读取,因为 RMW 保证它的 load 会 load 到最新的值,因此此时 #2 synchronize-with #5,皆大欢喜。
再说 c++11 内存模型的更多相关文章
- C++11 并发指南七(C++11 内存模型一:介绍)
第六章主要介绍了 C++11 中的原子类型及其相关的API,原子类型的大多数 API 都需要程序员提供一个 std::memory_order(可译为内存序,访存顺序) 的枚举类型值作为参数,比如:a ...
- c++11 内存模型解读
c++11 内存模型解读 关于乱序 说到内存模型,首先需要明确一个普遍存在,但却未必人人都注意到的事实:程序通常并不是总按着照源码中的顺序一一执行,此谓之乱序,乱序产生的原因可能有好几种: 编译器出于 ...
- C++11内存模型的一些补充阅读材料
<Intel Threading Building Block> O'REILLY Chapter 7 Mutual Exclusion - Atomic Operation - Memo ...
- C++11内存模型的粗略解释
基本解释 C++11引入了多线程,同时也引入了一套内存模型.从而提供了比较完善的一套多线程体系.在单线程时代,一切都很简单.没有共享数据,没有乱序执行,所有的指令的执行都是按照预定的时间线.但是也正是 ...
- C++11并发内存模型学习
C++11标准已发布多年,编译器支持也逐渐完善,例如ms平台上从vc2008 tr1到vc2013.新标准对C++改进体现在三方面:1.语言特性(auto,右值,lambda,foreach):2.标 ...
- Cocos2d-x v3.11 中的新内存模型
Cocso2d-x v3.11 一项重点改进就是 JSB 新内存模型.这篇文章将专门介绍这项改进所带来的新研发体验和一些技术细节. 1. 成果 在 Cocos2d-x v3.11 之前的版本中,使用 ...
- c++11 standardized memory model 内存模型
C++11 标准中引入了内存模型,其目的是为了解决多线程中可见性和顺序(order).这是c++11最重要的新特征,标准忽略了平台的差异,从语义层面规定了6种内存模型来实现跨平台代码的兼容性.多线程代 ...
- 【JVM.11】Java内存模型与线程
鲁迅曾经说过“并发处理的广泛应用是使得Amdahl定律代替摩尔定律成为计算机性能发展源动力的根本原因,也是人类‘压榨‘ 计算机运行能力的最有力武器.” 一.概述 多任务处理在现代计算机操作系统中几乎已 ...
- 11、Java并发性和多线程-Java内存模型
以下内容转自http://ifeve.com/java-memory-model-6/: Java内存模型规范了Java虚拟机与计算机内存是如何协同工作的.Java虚拟机是一个完整的计算机的一个模型, ...
随机推荐
- ubuntu安装ftp服务器
ubuntu安装ftp服务器 1: 安装vsftpd ~$ sudo apt-get install vsftpd ubuntu10.10自己装了,这步省略. 2: 配置vsftpd 2.1 修改vs ...
- paip.提升安全性----Des加密 java php python的实现总结
paip.提升安全性----Des加密 java php python的实现总结 /////////// uapi private static String decryptBy ...
- php中的邮件技术
前言:程序员不谈恋爱就是对物质世界太贪恋 一.通过smtp服务来发送邮件 使用php中的mail()实现邮件的发送; bool mail ( string $to , string $subject ...
- Leetcode 118 Pascal's Triangle 数论递推
杨辉三角,即组合数 递推 class Solution { vector<vector<int>> v; public: Solution() { ; i < ; ++i ...
- Hadoop MapReduce例子-新版API多表连接Join之模仿订单配货
文章为作者原创,未经许可,禁止转载. -Sun Yat-sen University 冯兴伟 一. 项目简介: 电子商务的发展以及电商平台的多样化,类似于京东和天猫这种拥有过亿用户的在线购 ...
- VS2015安装 Secondary Installer Setup Failed求解决方案
个人同步本文博客地址http://aehyok.com/Blog/Detail/64.html 个人网站地址:aehyok.com QQ 技术群号:206058845,验证码为:aehyok 本文文章 ...
- Delphi -- Compiler helper for initializing/finalizing variable
it CompilerhelperForInitializingFinalizingVariable; interface { Compiler helper for initializing/fin ...
- 在cxf中使用配置避免增加字段导致客户端必须更新、同步实体属性的问题
在使用cxf实现webservice时,经常碰到的问题就是如果在服务端,修改了一个接口的签名实现,如增加一个字段,或者删除一个字段.在这种情况下,在默认的配置中,就会报以下的错误信息: org.apa ...
- DRAM 内存介绍(二)
参考资料:http://www.anandtech.com/show/3851/everything-you-always-wanted-to-know-about-sdram-memory-but- ...
- 最近面试遇到的Windows相关的题目
上周准备在公司内部转岗,面了3个部门windows客户端相关的工作,最终拿到3个Offer,主要涉及C++和Windows两大块内容,C++的题目基本都答上了,Windows一直都是我的弱项,在这里记 ...