C++ 并发编程:C++11 原子操作与内存模型

一、需要了解的知识

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 中常用的内存模型

  • 顺序一致型
    memory_order_seq_cst对于atomic类型数据的内存顺序要求过高, 容易阻碍系统发挥线程应有的性能
	#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;
	}
  • 松散型
    memorey_order_relaxed对内存顺序毫无要求,无法保证程序的运行结果
	#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();
	}

注意:打破变量依赖链:std::kill_dependency

  • acquire-release型
    其他的如memory_order_acq_rel, 则是常用于实现一种叫做CAS(compare and swap) 的基本同步元语, 对应到atomic的原子操作compare_exchange_strong成员函数上。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值