C++11并发内存模型学习

    C++11标准已发布多年,编译器支持也逐渐完善,例如ms平台上从vc2008 tr1到vc2013。新标准对C++改进体现在三方面:1.语言特性(auto,右值,lambda,foreach);2.标准库(智能指针,容器,函数式编程);3.还有最重要,又最容易被忽视的改进,并发内存模型标准的制定。

    什么是内存模型?
    输入一定的内存操作序列,得到一定的内存结果,这种对应关系的集合,就是内存模型,任何一种CPU架构,都有该架构对应的内存模型。C++语言规范定义了一个虚拟机,这个虚拟机的指令集就是标准C++语句,虚拟机执行C++语句,对内存操作并得到相应的结果,这就是C++内存模型。直到C++03,C++都没有定义并发环境下的内存模型,之前的内存模型只适用于单线程。C++03并发环境下的内存模型,都是具体的编译器,运行库、操作系统及具体硬件定义。在并发环境下,同一序列的C++代码在不同编译器、不同运行环境、不同硬件平台上得到的内存结果可能会不一样。也就是说,到C++03为止,C++并发环境下的代码,不可移植,容易产生混乱。

    C++11的今天,程序员再没有免费的午餐,提升程序执行效率的手段必须与并发紧密结合,标准委员会已经意识到并发内存模型制定的重要性。并发内存模型制定后,任何一份符合C++标准的可并发代码(不包含特定平台API调用,特定平台汇编嵌入),在任何符合C++标准的编译器上编译,得到的二进制内容的运行时行为一致。

    并发不等于多线程!

    并发是指,多线程同时调度到多个cpu核心,并行执行。在以前单cpu核心系统上,多个线程的代码被限制在但个cpu核心上串行执行,所以C++03内存模型依然适用。多核心时代情况有所区别,不同线程的代码会并发执行,在这种情况下,内存(中的共享变量)在每个核心看来,是怎么样变化的?能看到变化吗?能看到变化的顺序一致吗?

    如果渴望能看到变化,或者更严格,看到变化的顺序也一致,就需要:缓存一致性和顺序一致性。

    缓存一致性:多核心系统,每个核心有私有的缓存,如果2个或以上核心的私有缓存引用了相同的内存位置,在并发执行情况下,就会出现有趣的现象。例如核心1刚把数据写到缓存(还没有回写内存),接着核心2读取内存,证核心2能读的到最新的数据吗?也就是说,核心2能看见核心1对内存的修改吗?缓存一致性机制保证了核心2能看到核心1对内存修改这一行为。http://en.wikipedia.org/wiki/Cache_coherence
有了缓存一致性,是不是就保证C++程序并发跨平台行为一致呢,不一定,因为从高级语言到机器码,隔了很多层。cpu实际执行的指令序列与源代码的语句序列有区别,于是引出另一个概念——顺序一致性。

    为什么强调顺序一致,因为:1.源代码顺序与编译出来的机器码顺序不一致;2.编译出来的机器码顺序与执行顺序不一致;3.并发执行时,每个核心观察到的总体指令序列不一致,或者说,内存改变的顺序不一致,这与cpu核心与核心之间共享数据同步策略有关。http://en.wikipedia.org/wiki/Sequential_consistency
什么情况下会破坏顺序一致性:a.编译器优化破坏顺序一致,例如为了优化cpu缓存效率而把load指令都集中一起先执行,再执行store指令的指令顺序优化,又例如提升执行效率的变量寄存器化;b.即使编译器不做任何优化,cpu也会执行优化,例如乱序执行,操作数预取等,这些行为破坏顺序一致性。顺序一致性对并发究竟产生什么影响?最简单设想:1.如果临界区锁仍然有效,但lock之前以及之后的代码被cpu乱序执行了,什么后果;2.又或者lock成功后,得到的结果并不能反映最近一次unlock的修改。

    C++11并发内存模型提供了什么给程序员?通过C++标准库,为程序员提供:“是否对代码启用顺序一致性及启用何种程度的顺序一致性”选项,这些选项就是std::memory_order。先了解几个概念再展开memory_order:

    1.synchronizes with:多核心情况下,核心1对内存变量a修改后,紧接着核心2读取内存a,如果核心2能读到核心1修改的内容,那么,变量a就具有synchronizes with属性。通过观察具有synchronizes with属性变量的变化顺序,能够得到核心之间执行指令的顺序,用以确定happens before关系。注意,并不是所有内存变量都具有synchronizes with属性,因为这些变量可能被编译器或者cpu优化掉。

    2.happens before:多核心情况下,如果指令A保证在指令B之前执行,就叫做A happens before B。并发环境下,happens before关系相当重要,例如lock及lock之后的代码就是happens before的关系。happens before关系与观测者有关,不同的观察者看到的顺序可以不同,也可以相同,将在展开memory_order_acquire和memory_order_release的时候举例子。

    3.acquire和release:与锁拥有类似的特性(这里的特性不是指阻塞,而是指指令序列),acquire与acquire之后的代码必然是happens before关系,acquire之后必然能够看见最近一次release操作所引起的内存变化;release操作完成后,在其他核心看来(其他核心必须使用acquire),该内存已产生变化,release之前的所有操作(源代码角度)在release之后都是有效——happens before。

    OK,逐一展开memory_order枚举

    memory_order_relaxed:最松散,或者说,不执行顺序一致性。该内存操作保证是原子操作,但可以在编译时优化或在运行时被cpu执行任意优化。而且在多核心平台上,relax操作产生的结果可能不会被立即同步到其他核心(没有synchronizes with属性),其他核心不会立即看见这个操作。relax的原子操作与上下文代码没有happens before的关系,也就是没有release语意。

    先看看memory_order_acquire和memory_order_release:这对操作具有acquire和release语义,被操作的内存变量在实施操作的核心之间有synchronizes with属性,可以保证源代码及编译后机器码顺序一致,但这种保证仍然不够严格,看这个例子:

 -Thread 1-  y.store (20, memory_order_release);

 -Thread 2-  x.store (10, memory_order_release);

 -Thread 3-  assert (y.load (memory_order_acquire) == 20 && x.load (memory_order_acquire) == 0)

 -Thread 4-  assert (y.load (memory_order_acquire) == 0 && x.load (memory_order_acquire) == 10)

运行时,两个assert都可能同时不触发,在t3角度看来xy的改变顺序与t4角度允许不一致,xy顺序无关,无论在源代码的角度,还是机器码的角度。如果没有t3和t4的观察,xy的执行顺序既是x-y,也是y-x,是不是跟那个什么猫的实验很像。如果要所有核心看来顺序一致,就要用memory_order_seq_cst。

    再看看memory_order_consume:与acquire类似,但不会涉及到代码上下文内与原子操作不相关的内存变量,看这个例子:

-Thread 1-  

n = 1  

m = 1

p.store(&n, memory_order_release)

-Thread 2-  

t = p.load(memory_order_acquire);  

assert( *t == 1 && m == 1 );

-Thread 3-  

t = p.load(memory_order_consume);  

assert( *t == 1 && m == 1 );

t3会fail,因为p与m无关,(p只与n有关),所以t3角度只得到n,p的release结果,m没有被同步,相反,t2会同步n、m、及p。

    memory_order_acq_rel:这个不用介绍了。

    memory_order_seq_cst:最严格的顺序一致性,所有标记seq_cst的操作,顺序都不能被打乱(不能被编译器优化,不能被cpu乱序),无论从代码角度(源代码)还是cpu指令流(并发执行)的角度。在任意一个核心看来,所有标记为seq_cst的操作的顺序都一致。

    说了这么多,C++11并发内存模型究竟起到什么作用,没有C++11并发内存模型,以前并发代码仍然可用!是的,因为以前的代码用了系统相关的锁,带有acquire和release语义,不过,这些代码不可移植。试想我们要编写一个用户模式下的spinlock,怎么做?vc下可以用interlockxxx这类编译器内置函数完成,你没看错,interlockxxx既不是C++标准,也不是windows API,它是ms c++编译器的内置函数(msdn remark有这样一段This function is implemented using a compiler intrinsic where possible),用于指导编译器生成顺序一致性代码。有了C++11并发内存模型,你就可以用标准库atomic和memory_order的memory_order_acquire、memory_order_release完成spinlock的编写,既符合C++标准,也具备一致性,而且代码的行为也得到保证。

参考

http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1525.htm

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2338.html

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2480.html

http://gcc.gnu.org/wiki/Atomic/GCCMM/AtomicSync

http://gcc.gnu.org/wiki/Atomic/GCCMM/AtomicTypes

http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-vol-3a-part-1-manual.pdf

8.2.2及8.2.3章节

http://msdn.microsoft.com/en-us/library/windows/desktop/ms686355(v=vs.85).aspx

http://msdn.microsoft.com/en-us/library/windows/hardware/ff540496(v=vs.85).aspx

 

你可能感兴趣的:(内存模型)