一、需要了解的知识
1,影响最后程序输出结果的因素有以下几个
-
代码的书写顺序(这是废话了…)
这里不做赘述:但是给出一个C++代码的示例:
#include <thread> #include <atomic> #include <iostream> using namespace std; atomic<int> a {0}; atomic<int> b {0}; int ValueSet(int) { int t = 1; a = t; b = 2; } int Observer(int) { cout << "(" << a << ", " << b << ")" << endl; // 可能有多种输出 } int main() { thread t1(ValueSet, 0); thread t2(Observer, 0); t1.join(); t2.join(); cout << "Got (" << a << ", " << b << ")" << endl; // Got (1, 2) }
俗话说得好好,你看到的代码顺序不一定是最后运行的结果…
-
强弱内存模型平台CPU对机器指令的执行顺序
-
内存模型
内存模型通常是一个硬件上的概念, 表示的是机器指令(或者读者将其视为汇编语言指令也可以) 是以什么样的顺序被处理器执行的. -
强内存模型(比如x86):cpu对生成的机器指令顺序执行
-
弱内存模型(比如PowerPC):cpu对生成的机器指令可以不按照顺序执行(乱序执行)
-
注意:为什么会有弱顺序的内存模型?
简单地说 弱顺序的内存模型可以使得处理器进一步发掘指令中的并行性, 使得指令执行的性能更高. -
以下是上面示例代码中“t=1;a=t;b=2;”生成的伪汇编代码(这里近似看出机器指令)
1: Loadi reg3, 1; # 将立即数1放入寄存器reg3 2: Move reg4, reg3; # 将reg3的数据放入reg4 3: Store reg4, a; # 将寄存器reg4中的数据存入内存地址a 4: Loadi reg5, 2; # 将立即数2放入寄存器reg5 5: Store reg5, b; # 将寄存器reg5中的数据存入内存地址b
强内存模型cpu执行顺序是:总是1->2->3->4->5
弱内存模型cpu执行的顺序是:可能是1->2->3->4->5,由于指令1、 2、 3和指令4、5运行顺序上毫无影响(使用了不同的寄存器以及不同的内存地址),所以也可能是 1->4->2->5->3 -
-
编译器的编译优化
-
编译器会根据代码运行的强弱内存模型对最后编译生成的机器指令(这里近似当做汇编指令) 进行指令重排序以及对弱内存模型需要一定执行顺序的指令加上内存栅栏。
-
强内存模型生成的汇编指指令
因为a和b的原子变量默认的是采取顺序一致性原则,即禁止编译器对a b相关的指令的重排序优化,所以强内存模型cpu的执行顺序总是1->2->3->4->5
1: Loadi reg3, 1; # 将立即数1放入寄存器reg3 2: Move reg4, reg3; # 将reg3的数据放入reg4 3: Store reg4, a; # 将寄存器reg4中的数据存入内存地址a 4: Loadi reg5, 2; # 将立即数2放入寄存器reg5 5: Store reg5, b; # 将寄存器reg5中的数据存入内存地址b
-
弱内存模型生成的汇编指令
因为a和b的原子变量默认的是采取顺序一致性原则,即禁止编译器对a b相关的指令的重排序优化,所以生成的汇编代码和强内存模型生成的一致,但是由于弱内存模型cpu是乱序执行的,所以还需要编译器加入内存栅栏,强制指令执行顺序为
1->2->3->4->5.
sync:该指令迫使已经进入流水线中的指令都完成后处理器才执行sync以后指令(排空流水线)。这样一来,sync之前运行的指令总是先于sync之后的指令完成的.
1: Loadi reg3, 1; # 将立即数1放入寄存器reg3 2: Move reg4, reg3; # 将reg3的数据放入reg4 3: Store reg4, a; # 将寄存器reg4中的数据存入内存地址a 4: Sync # 内存栅栏 5: Loadi reg5, 2; # 将立即数2放入寄存器reg5 6: Store reg5, b; # 将寄存器reg5中的数据存入内存地址b
-
二、C++11 内存模型
1、同步发生和先行发生
- 同步发生
只能在原子类型之间进行的操作。 例如对一个数据结构进行操作(对互斥量上锁), 如果数据结构包含有原子类型, 并且操作内部执行了一定的原子操作, 那么这些操作就是同步发生关系。 从根本上说, 这种关系只能来源于对原子类型的操作。
举例: 如果线程A存储了一个值, 并且线程B读取了这个值, 线程A的存储操作与线程B的载入操
作就是同步发生的关系. - 先行发生
它指定了某个操作去影响另一个操作。 对于单线程来说, 就简单了: 当一个操作排在另一个之后, 那么这个操作就是先行执行的。 这意味着, 如果源码中操作A发生在操作B之前, 那么A就先行与B发生。
2、内存模型枚举
枚举值 | 定义规则 |
---|---|
memory_order_relaxed | 不对执行顺序做任何保证 |
memory_order_acquire | 本线程中所有的后续读操作必须在本条原子操作完成后执行 |
memory_order_release | 本线程中所有之前的写操作完成后才能执行本条原子操作 |
memory_order_acq_rel | 同时包含memory_order_acquire和memory_order_release标记 |
memory_order_consume | 本线程中所有后续有关本原子类型的操作,必须在本条原子操作完成之后执行 |
memory_order_seq_cst | 全部存取都按顺序执行 |
3、内存模型的简单分类
-
原子存储操作(store):可以使用memorey_order_relaxed、memory_order_release、 memory_order_seq_cst。
-
原子读取操作(load): 可以使用memorey_order_relaxed、memory_order_consume、 memory_order_acquire、memory_order_seq_cst。
-
RMW操作(read-modify-write): 即一些需要同时读写的操作, 比如之前提过的atomic_flag类型的test_and_set()操作。 又比如atomic类模板的atomic_compare_exchange()操作等都是需要同时读写的。 RMW操作可以使用memorey_order_relaxed、 memory_order_consume、memory_order_acquire、 memory_order_release、 memory_order_acq_rel、memory_order_seq_cst。
-
一些形如“operator=”、 “operator+=”的函数, 事实上都是memory_order_seq_cst作为memory_order参数的原子操作的简单封装。
4、C++11 中常用的内存模型
#include <thread>
#include <atomic>
#include <iostream>
using namespace std;
atomic<int> a;
atomic<int> b;
int Thread1(int) {
int t = 1;
a.store(t, memory_order_seq_cst);
b.store(2, memory_order_seq_cst);
}
int Thread2(int) {
while(b.load(memory_order_seq_cst) != 2); // 自旋等待
cout << a.load(memory_order_seq_cst) << endl;
}
int main() {
thread t1(Thread1, 0);
thread t2(Thread2, 0);
t1.join();
t2.join();
return 0;
}
#include <thread>
#include <atomic>
#include <iostream>
using namespace std;
atomic<int> a;
atomic<int> b;
int Thread1(int) {
int t = 1;
a.store(t, memory_order_relaxed);
b.store(2, memory_order_relaxed);
}
int Thread2(int) {
while(b.load(memory_order_relaxed) != 2); // 自旋等待
cout << a.load(memory_order_relaxed) << endl;
}
int main() {
thread t1(Thread1, 0);
thread t2(Thread2, 0);
t1.join();
t2.join();
return 0;
}
-
release-acquire型
a.store先于发生b.store
b.load先于发生a.laod
完全保证了代码运行的正确性, 即当b的值为2的时候, a的值也确定地为1。 而打印语句也不会在自旋等待之前打印a的值
#include <thread>
#include <atomic>
#include <iostream>
using namespace std;
atomic<int> a;
atomic<int> b;
int Thread1(int) {
int t = 1;
a.store(t, memory_order_relaxed);
b.store(2, memory_order_release); // 本原子操作前所有的写原子操作必须完成
}
int Thread2(int) {
while(b.load(memory_order_acquire) != 2); // 本原子操作必须完成才能执行之后所有的读原子操作
cout << a.load(memory_order_relaxed) << endl; // 1
}
int main() {
thread t1(Thread1, 0);
thread t2(Thread2, 0);
t1.join();
t2.join();
return 0;
}
-
release-consume型
这样的内存顺序保证了ptr.load(memory_order_consume)必须发生在*ptr这样的解引用操作之前
并不保证发生在data.load(memory_order_relaxed)之前
#include <thread>
#include <atomic>
#include <cassert>
#include <string>
using namespace std;
atomic<string*> ptr;
atomic<int> data;
void Producer() {
string* p = new string("Hello");
data.store(42, memory_order_relaxed);
ptr.store(p, memory_order_release);
}
void Consumer() {
string* p2;
while (!(p2 = ptr.load(memory_order_consume)))
;
assert(*p2 == "Hello"); // 总是相等
assert(data.load(memory_order_relaxed) == 42); // 可能断言失败
}
int main() {
thread t1(Producer);
thread t2(Consumer);
t1.join();
t2.join();
}