Category: Software Engineering Practices
-
C++11中atomic类型和内存模型
本篇文章介绍C++11中的原子变量的内存模型,内存模型比较复杂,与多线程、CPU缓存以及指令优化都有关系,这里记录一下现在的理解,如有不正确的地方欢迎批评指正。后续也会进一步更新更深入的理解。 C++11是在C/C++99之后出现的变动较大的C++版本,一般Mordern C++是指C++11以后的C++。Modern C++有很多新的特性,其中automic变量(原子变量)就是一种支持的新的特殊类型。所谓原子变量,顾名思义,针对这些变量的操作都是原子操作(完整不可分),这为多线程程序中的共享变量的实现提供了一个便捷的方法。C++11 对常见的原子操作进行了抽象,定义出统一的接口,并根据编译选项/环境产生平台相关的实现。新标准将原子操作定义为atomic模板类的成员函数,囊括了绝大多数典型的操作——读(load)、写(store)、CAS(Compare and Swap, 比较并替换,如compare_exchange_weak)等。 现代的处理器并不是逐条处理机器指令的。内存模型是一个硬件上的概念,表示的是机器指令(或者将其视为汇编指令也可以)是以什么样的顺序被处理器执行的。指令1、2、3(a赋值)和指令4、5(b赋值)毫不相干。一些处理器可能将指令乱序执行,比如按照1->4->2->5->3这样的顺序(超标量流水线,即一个时钟周期里发射多条指令)。如果指令是“乱序”执行的,我们称这样的内存模型为弱顺序的(weak ordered)。弱顺序的内存模型的好处在可以进一步挖掘指令中的并行性,提高指令执行的性能。 顺序一致内存顺序/memory_order_seq_cst,memory_order_seq_cst 表示该原子操作必须顺序一致的,这是C++11中所有atomic原子操作的默认值。这样来理解“顺序一致”:即代码在线程中运行的顺序与程序员看到的代码顺序一致。也就是说,用此值提示编译器“不要给我重排序指令,不要整什么指令乱序执行,就按照我代码的先后顺序执行机器指令”。在示例代码中,a的赋值语句先于b的赋值语句执行,这种称之为”先于发生(happens-before)“关系。用memory_order_seq_cst 可以确保这种happens_before关系。 松散内存顺序memory_order_relaxed,表示该原子操作指令可以任由编译器重排或者由处理器乱序执行。和普通变量的对编译器的表现没有什么差异。 上面讲的顺序一致和松散方式对应着两个极端——一个是严格禁止”指令乱序加速优化“,一个是允许随便”指令乱序加速优化“。但是现实的逻辑问题是:严格禁止”指令乱序优化“,指令执行不够快;允许随便”指令乱序优化“,在多线程程序场景下很可能又违反了计算的正确逻辑得不到正确结果。 Release-acquire内存顺序,memory_order__acquire规则定义:本线程中,所有后续的读写操作,必须在本条原子操作完成后执行。(本线程中,读写操作在本代码后面的要保证我先读),memory_order_release规则定义:本线程中,所有之前的内存读写操作完成后,才能执行本原子写操作。(在本线程中,写操作在本代码前面的要保证你们先写,我最后写……)下面有一段示例代码说明。memory_order_release,Perform release operation. Informally speaking, prevents all preceding memory operations to be reordered past this point.;memory_order_acquire,Perform acquire operation. Informally speaking, prevents succeeding memory operations to be reordered before this point.memory_order_acq_rel,Perform both release and acquire operation. memory_order_acquire & memory_order_release成对作用在一个原子对象上,能保证在多个线程中对这个原子对象的修改和读取是一致的。 memory_order_acquire & memory_order_release 相比 relaxed序具有更大的同步开销。 上述的直观的理解这么解释:关于原子变量的load读操作如果设为memory_order_acquire,在本线程内后面的内存读写必须等待读操作完成后才能进行,同理原子变量的store写操作如果设为memory_order_release,则在本线程内前面的内存读写必须在store写操作之前完成。两个不同的线程共享的原子变量a,如果在线程1中成功load到线程2store后的状态,则线程2中store之前的共享的内存数据状态对于线程1是可见的。如上面的代码所示,只有Thread1中的b.store写入2后,Thread2读取的b的状态才是2,这时才会退出while循环,这时Thread2的后面的代码中a的状态就是Thread1中保存的值1(因为Thread1中保证a.store会在b.store之前完成。 C++标准库中的memory order其实与具体的machine无关的,在实现上会通过memory barrier(FENCE)来进行order,memory barrier在不同CPU类型上表现不一定一致。 CAS的操作以compare_exchange_weak函数为例进行说明, The memory…