在网上看了不少memory order的文章,结果越看越糊涂。本觉得懂了,结果碰到问题仍是不懂。反反复复,最终才造成一套能够逻辑自洽的解释。记录在此,既但愿减小后来者被错误文档误导的痛苦,也但愿有高手能够不吝赐教指点一二。android
在阐述memory order以前,首先须要介绍memory model,中文翻译为“内存模型”。关于这个概念,网上有很多文不对题的解释,尤为是Java的memory model。很多文章将将它理解为JVM中各块内存区域的分布和做用,实际上是张冠李戴了。c++
C++内存模型和Java内存模型同样,都属于语言层面的抽象规范。而memory order则属于其中的子概念,于C++11中正式被引入。它定义了一个原子操做与其附近全部其余与memory交互的操做之间的重排(reorder)限制。为了方便理解,其定义能够被拆分为如下几个小点来细化:程序员
对于C++而言,普通开发者是不须要也不会接触到memory order这个概念的,由于他们被保护得很好。这里的“保护”指的是:编程
既然如此,那memory order这个概念为何还会诞生?缘由也有两个:markdown
Memory order总共有6种类型,但这里我只准备介绍4种。memory_order_consume和memory_order_acq_rel被排除在外的缘由以下:并发
接下来首先出场的是memory_order_acquire和memory_order_release。memory_order_acquire只用于原子化的load(读操做)操做,而memory_order_release只用于原子化的store(写操做)操做。其写法一般以下所示:函数
std::atomic<int> x;
x.load(std::memory_order_acquire);
x.store(1, std::memory_order_release);
复制代码
memory_order_acquire禁止了该load操做以后的全部读写操做(不论是原子对象仍是非原子对象)被重排到它以前去运行。oop
memory_order_release禁止了该store操做以前的全部读写操做(不论是原子对象仍是非原子对象)被重排到它以后去运行。性能
在同一个原子化对象上使用这两种memory order将会获得一个额外的好处,即两个线程在知足某种条件时将会拥有特定的数据可见性。这句话比较拗口,下面用图示来展开说明。优化
当flag.load在时间上晚发生于flag.store时,Thread 1上flag.store以前的全部操做对于Thread 2上flag.load以后的全部操做都是可见的。若是flag.load发生的时间早于flag.store,那么两个线程间则不拥有任何数据可见性。
为了保证flag.load在时间上晚发生于flag.store,咱们能够经过if逻辑来进行选择。所以,下面的写法将会永远assert经过。
接着咱们考虑当全部Atomic对象的读都采用memory_order_acquire,写都采用memory_order_release时,两个不一样Atomic对象的操做之间是否会发生重排。
读写操做之间的关系总共有四种:读读,读写,写写,写读。对于前三种操做关系,memory_order_acquire和memory_order_release均可以保证两条针对原子对象的操做不发生重排。但针对最后一种操做关系“写读”则没法保证。缘由是前面一条指令是store,memory_order_release只能保证store以前的指令不重排到store以后,却没法禁止位于其后的load指令重排到它前面;后面一条指令是load,memory_order_acquire只能保证load以后的指令不重排到load以前,却没法禁止位于其前的store指令重排到它后面。最终store指令将有可能重排到load指令以后,这种没法禁止的重排关系咱们简称为SL。
Memory_order_acquire和memory_order_release是程度中等的memory order,比他们强一些的就叫作memory_order_seq_cst(sequential consistent),它只比memory_order_release/memory_order_acquire多一个功能,便可以禁止SL的重排。而memory_order_release则至关于自废武功,两个不一样原子对象间的操做能够随便重排,它只保证针对同一个原子对象的操做不发生重排。
ART虚拟机实现了本身的Mutex,其中最关键的函数即是Mutex::ExclusiveLock和Mutex::ExclusiveUnlock。Android Q以前的Mutex实现代码以下。
Mutex::ExclusiveLock:
其中最关键的操做是①和②:
①表示该Mutex多了一个竞争者,因为是原子化对象的++操做,所以采用默认的memory order: memory_order_seq_cst。
②是一个RMW(Read-Modify-Write)操做,它会读取state_的值,并和1进行比较。若是相等则将此线程挂起;若是不相等则直接返回0,让该线程从新判断是否能够得到Mutex。因为是默认的load操做,所以也采用memory_order_seq_cst。
Mutex::ExclusiveUnlock:
其中最关键的操做是③和④:
③也是一个RMW操做,它会读取state_的值,并和cur_state进行比较。若是相等,则令state_等于0;若是不相等,则返回false。因为是默认操做,因此load和store都采用memory_order_seq_cst。
④读取num_contenders的值,可是传入了memory_order_relaxed,代表对该操做作了最弱的重排限制。
上面的代码在多数状况下都没有问题,可是按照以下的顺序执行便会出错。
Thread 1执行解锁的操做,Thread 2执行上锁的操做。
因为③是RMW操做,实质上能够拆分为多条指令,③.a和③.b表示其中的load和store操做。CompareAndSet虽然是原子化操做,可是它只保证在执行过程当中该原子对象的值不会被外界改动。至于其余指令是否能够重排到③.a和③.b之间,则由具体的memory order决定。
操做④能够被重排在③.a和③.b之间的缘由:
虽然③.a和③.b的memory order为memory_order_seq_cst,可是当重排的另外一个操做不是memory_order_seq_cst修饰的原子化操做时,memory_order_seq_cst便退化成了memory_order_acquire或memory_order_release(取决于操做是load仍是store)。所以③.a只能限定④不重排到它以前,而③.b对④则没有任何重排限制。所以,④能够被重排到③.a和③.b之间。
从Thread 1的视角看,①和②都是针对原子对象的操做,所以两者的执行顺序必须等同代码顺序,也即①在②以前执行。另外①②在③.a和③.b中间执行并不影响③的原子性,所以也是被容许的。
一旦程序按照这样的顺序执行,便会致使Thread 2释放锁但不唤醒Thread 1,以致于Thread 1一直睡下去。而究其缘由,这一切都是由④重排到③.a和③.b之间致使的。
如今咱们从新思考③.b和④之间的关系。
③.b是一个原子化对象的store操做,④是一个原子化对象的load操做。这是一个典型的SL(store load)情形,而限制他们被重排只有一种方法:将两者都用memory_order_seq_cst修饰。
所以解决的方案也比较简单,也即将num_contenders_的memory order改成memory_order_seq_cst。
固然,这么更改之后会对性能产生必定的影响。由于Mutex在虚拟机中被大量的使用,它任何小小的改动都会影响深远。正是由于这个缘由,Google在Android Q上对Mutex的实现进行了优化,将num_contenders_和state_合并为一个原子化对象,这样就不存在两个不一样原子化对象操做之间的重排关系了。合并后的原子化对象叫作state_and_contenders_,其最低位的0或1表明state_,而高位的数字除以2便表明num_contenders_。
具体的change在这里,感兴趣的伙伴能够继续研究。