现代多核CPU的cache模型基本都跟下图1所示同样,L1 L2 cache是每一个核独占的,只有L3是共享的,当多个cpu读、写同一个变量时,就须要在多个cpu的cache之间同步数据,跟分布式系统同样,必然涉及到一致性的问题,只不过二者之间共享内容的方式不同而已,一个经过共享内存来共享内容,另外一个经过网络消息传递来共享内容。就像wiki所说起的:c++
Interestingly enough, a shared-memory multiprocessor system really is a message-passing computer under the covers. This means that clusters of SMP machines that use distributed shared memory are using message passing to implement shared memory at two different levels of the system architecture.缓存
图一、现代cpu多级cache安全
多核一致性最典型的应用场景是多线程的原子操做,其在多线程开发中常常用到,好比在计数器的生成,这类状况下数据有并发的危险,可是用锁去保护又显得有些浪费,因此原子类型操做十分的方便。网络
原子操做虽然用起来简单,可是其背景远比咱们想象的要复杂。其主要在于现代计算系统过于的复杂:多处理器、多核处理器、处理器又有核心独有以及核心共享的多级缓存,在这种状况下,一个核心修改了某个变量,其余核心何时可见是一个十分严肃的问题。同时在极致最求性能的时代,处理器和编译器每每表现的很智能,进行极度的优化,好比什么乱序执行、指令重排等,虽然能够在当前上下文中作到很好的优化,可是放在多核环境下经常会引出新的问题来,这时候就必须提示编译器和处理器某种提示,告诉某些代码的执行顺序不能被优化。今天咱们重点看一下处理器在多线程原子操做上的背景原理以及具体应用。多线程
考虑下面典型的代码:并发
-Thread 1- void foo(void) { a = 1; b = 1; } -Thread 2- void bar(void) { while (b == 0) continue; assert(a == 1); }
因为cpu cache的存在,thread 2在断言处可能会失败。具体的,因为各个CPU的cache是独立的,因此变量在他们各自的cache里面的顺序可能跟代码的顺序是不一致的,也就是说执行thread2的cpu可能会先看到变量b的变化,而后再看到变量a的变化,致使断言失败。就是咱们常见的program order与process order的不一致的工程现象,这里就涉及到了memory consistency model的问题(相似于分布式系统的一致性)。app
上述的代码若是要正确执行,则变量a、b之间须要有‘happen before’的语义来约束(这里就能够联想到分布式系统中因果一致性的概念)。可是对于这个语义上的需求,硬件设计者也心有余而力不足,由于CPU没法知道变量之间的关联关系。因此硬件设计者提供了memory barrier指令,让软件能够经过这些指令来告诉CPU这类关系,实现program order与process order的顺序一致。相似于下面的代码:分布式
-Thread 1- void foo(void) { a = 1; memory_barrier(); b = 1; }
增长memory barrier以后,就能够保证在执行b=1的时候,cpu已经处理过'a=1'的操做了。也就是说经过硬件提供的memory barrier语义,使得软件可以保证其以前的内存访问操做先于其后的完成。memory barrier 经常使用的地方包括:实现内核的锁机制、应用层编写无锁代码、原子变量等。下面咱们一块儿看下,c++11是怎样使用内存屏障来实现原子操做的。函数
在C++11标准出来以前,C++标准没有一个明确的内存模型,各个C++编译器实现者各自为政,随着多线程开发的普及解决这个问题变得愈来愈迫切。在标准出来以前,GCC的实现是根据Intel的开发手册搞出的一系列的__sync原子操做函数集合,具体以下:性能
type __sync_fetch_and_OP (type *ptr, type value, ...) type __sync_OP_and_fetch (type *ptr, type value, ...) bool__sync_bool_compare_and_swap (type *ptr, type oldval, type newval, ...) type __sync_val_compare_and_swap (type *ptr, type oldval, type newval, ...) __sync_synchronize (...)
在C++11新标准中规定的内存模型(memory model)颗粒要比上述的内存模型细化不少,因此软件开发者就有不少的操做空间了,若是熟悉这些内存模型,在保证业务正确的同时能够将对性能的影响减弱到最低,在硬件资源吃紧的地方,这是咱们优化程序的一个重要方向。
咱们以c++11的原子变量的保证来展开这些内存模型。原子变量的通用接口使用store()和load()方式进行存取,能够额外接受一个额外的memory order参数,这个参数就是对应了c++11的内存模型,根据执行线程之间对变量的同步需求强度,新标准下的内存模型能够分红以下几类:
Sequentially Consistent
该模型是最强的同步模式,参数表示为std::memory_order_seq_cst,同时也是默认的模型。
-Thread 1- y = 1 x.store (2); -Thread2- if(x.load() ==2) assert (y ==1)
对于上面的例子,即便x和y是不相关的,一般状况下处理器或者编译器可能会对其访问进行重排,可是在seq_cst模式下,x.store(2)以前的全部memory accesses都发生在store操做以前。同时,x.load()以后的全部memory accesses都发生在load()操做以后,也就是说seq_cst模式下,内存的限制是双向的。
Acquire/Release Consistent
std::atomic<int> a{0}; intb =0;
-Thread 1- b = 1; a.store(1, memory_order_release); -Thread 2- while(a.load(memory_order_acquire) !=1)/*waiting*/; std::cout<< b <<'\n';
毫无疑问,若是是memory_order_seq_cst内存模型,那么上面的操做必定是成功的(打印变量b显示为1)。
1. memory_order_release保证在这个操做以前的memory accesses不会重排到这个操做以后去,可是这个操做以后的memory accesses可能会重排到这个操做以前去。一般这个主要是用于以前准备某些资源后,经过store+memory_order_release的方式”Release”给别的线程;
2. memory_order_acquire保证在这个操做以后的memory accesses不会重排到这个操做以前去,可是这个操做以前的memory accesses可能会重排到这个操做以后去。一般经过load+memory_order_acquire判断或者等待某个资源,一旦知足某个条件后就能够安全的“Acquire”消费这些资源了。
这个就是相似于分布式系统的因果一致性的概念。
Relaxed Consistent
这个是最宽松的模式,memory_order_relaxed没有happens-before的约束,编译器和处理器能够对memory access作任何的re-order,所以另外的线程不能对其作任何的假设,这种模式下能作的惟一保证,就是一旦线程读到了变量var的最新值,那么这个线程将再也见不到var修改以前的值了(这个相似于分布式系统单调读保证的概念)。
这种状况一般是在须要原子变量,可是不在线程间同步共享数据的时候会用,同时当relaxed存一个数据的时候,另外的线程将须要一个时间才能relaxed读到该值(也就是最终若是变量再也不更改的话,全部的线程仍是能够读取到变量最终的值的),在非缓存一致性的构架上须要刷新缓存。在开发的时候,若是你的上下文没有共享的变量须要在线程间同步,选用Relaxed就能够了。
这一点相似于分布式系统的最终一致性概念了。
上述的过程体现的是强一致性、因果一致性、最终一致性等概念在c++11原子操做的使用,以及当前技术圈很是热门的话题分布式系统开发中分布式一致性概念的思考与迁移。从中咱们能够看出技术在发展,可是不少概念实际上是一脉相承的,只有深入理解了概念背后的原理以及相关技术发展的背景,才能勉强跟上技术的发展浪潮。