浅谈原子操做、volatile、CPU执行顺序

浅谈原子操做、volatile、CPU执行顺序

在计算机发展的鸿蒙年代,程序都是顺序执行,编译器也只是简单地翻译指令,随着硬件和软件的飞速增加,原来的工具和硬件渐渐地力不从心,也逐渐涌现出各路大神在原来的基础上进行优化,有些优化是彻底地升级,而有些优化则是创建在牺牲其余性能之上,固然这种优化在大多数状况下是正向的,只是在某些时候会体现出负面的效果,今天咱们就来谈谈那些因为软硬件的优化产生的问题。html

原子操做和锁机制

学过C语言的咱们都知道一个概念:程序是顺序执行的。可是因为操做系统的存在,这个概念变成了局部适用,由于操做系统的工做就是让多任务并发运行(单CPU下,虽然底层仍然是顺序执行,至少从用户角度来讲,任务是并发运行的)。linux

操做系统的发明简直就是一次革命,尤为是桌面操做系统的盛行,对人类设备的发展起到了很是大的做用。程序员

对于单核CPU而言,操做系统实现多任务的方式就是经过中断将时间不断地分片,经过某种调度手法,让多个任务循环地占用CPU的执行时间,形成多任务在同时运行的假象,固然,前提是CPU的运行频率足够快,用户感受不到任务之间的切换。算法

可是这就带来一个问题,若是某个任务须要作一个不能被打断的任务,反而没那么容易。缓存

既然提出问题,固然就有解决方案,就是原子操做。多线程

原子操做

咱们在化学课上学到,在目前的知识体系下,原子是不可分割的,原子操做所以而得名。并发

它代表:操做要么不进行,要么就直到执行完,不会被其余线程打断。在linux下,定义了两种原子操做,分别是int型变量操做和位操做方式。工具

它的定义是这样的:性能

typedef struct {
    int counter;
} atomic_t;

能够看到,atomic_t类型的原子操做就是针对int型变量进行操做。优化

原子操做的API:

atomic_read(atomic_t *v)             //读原子变量
atomic_set(atomic_t *v,int i)       //设置原子变量值
atomic_add(int i, atomic_t *v)       //原子变量加i
atomic_sub(int i, atomic_t *v)       //原子变量减i
....                                 //

看到这里有些朋友就有疑问了,int变量的操做难道不就是原子操做吗?就像:

int i = 0 
i++

这应该是原子操做吧。

可是事实并不是如此,int i = 0,这条赋值语句确实是赋值语句,可是i++并不是是原子操做,它包含如下的操做:

一、从内存中取出i
二、i加1
三、将i写回内存

那么,为何须要原子操做?咱们能够参考上面的三个步骤,若是在执行完第一步时,另外一个线程被调度执行,恰好也操做到i(i为全局变量),操做完以后再回到当前线程,此时i的值仍是在原来的值上加1,这就违背了程序的意图:咱们能够看下面的过程,设i为全局变量,初始值为5.

能够看到,若是不进行原子操做,i经历了两次++,值仅仅是增长了1,显然有问题,因此须要用到原子操做。

除了int型原子操做,linux下还支持设置位的原子操做,本文并不去细究原子操做细节,就再也不赘述。

锁机制

上述原子操做的效果能够当作:要操做的数据在操做开始直到操做结束,不会被其余任务所影响,从数据的角度而不是操做的角度出发,还有一种机制能够实现防止数据被其它任务破坏,就是锁机制,通常是使用互斥锁。

可是须要注意的是,锁机制并非保证原子操做,它只是防止其余任务操做不想被操做的数据,并且咱们须要知道的是,计算机的运行就是对数据的处理,锁住的对象应该是数据而非指令,不一样于原子操做的是,在操做加锁数据时,可能出现任务调度而执行其余程序,可是它能够保护数据不被其余程序操做,在执行效果上与原子操做实际上是大同小异的,即一个任务要处理部分数据,从操做开始到操做完成,这部分数据都只会被当前任务所影响。

同时,原子操做目前在各大平台上通常都只支持int型和bit操做,对于复杂的,大块的数据,原子操做显然力不从心。

volatile

在数据操做的多线程同步上是否是使用了原子操做就万事大吉了呢?非也非也!!

gcc编译器为了提升执行效率,和硬件相配合作了一件事,就是采用缓存机制,缓存数据的好处就是提升效率,具体的操做就是对代码进行优化。

当程序在运行时,若是每次读写数据都直接从内存读写,效率很快就达到瓶颈,编译器在这里作的优化就是若是一个数据会被频繁使用,就会被缓存在寄存器或者高速缓存中,下一次再使用的时候就不须要从新从内存中读取,直接从寄存器或者高速缓冲中读取便可,这样就大幅减小了数据访问时间,达到提升程序运行效率的效果。

可是,在多线程和中断程序的的环境下,这种编译器执行的优化将会带来同步问题。

要理解这种同步问题,首先咱们须要创建一个概念:多线程共享数据空间,可是拥有各自的栈数据和寄存器数据备份,当轮到本线程运行时,系统将上一线程的寄存器数据,堆栈数据保存起来,而后将当前线程的运行数据恢复到寄存器中,设置PC指针跳转执行当前线程。

咱们看下面的例子:

从上面的例子能够看到,尽管线程中使用锁机制来保障数据在操做时不被其余任务所干扰,可是因为数据被缓存而致使线程之间的数据出现同步问题。

话说回来,编译器也不至于那么蠢,对于任何指令都无脑进行缓存的优化,它会在缓存数据的同时会分析数据之间的依赖关系,可是,在多线程或者中断程序中,因为程序的执行是一种非预约义行为,编译器的优化可能并不能考虑到这一点,因此,在编写多线程程序时,对于跨线程的全局变量,或者在中断中的全局变量,必定要用volatile进行修饰,否则将发生很是难以调试的bug。

至此,编译器开发人员渐渐意识到这种优化带来的问题,便增长了一个关键字:volatile,volatile关键字声明的数据,即告诉编译器,全部由volatile修饰的数据都不要进行优化,每次的读写都老老实实地从内存中读出而后写回。

至于使用方法,和static const同样:

volatile int x;

volatile修饰和原子操做

咋一看,这两个东西好像是同样的,可是仔细一瞧,咱们仍是能够看出他们之间的区别:

  • 原子操做和锁强调的是数据在任务执行过程当中不会被其余任务操做到而产生冲突。

  • volatile关键词的修饰则强调数据在存取时直接操做内存,而不要缓存机制对其进行缓存。

一个解决的是同时操做带来的问题,一个解决的是操做完以后是否缓存(编译器优化)带来的问题。

CPU的乱序执行

在计算机工程领域,乱序执行(错序执行,英语:out-of-order execution,简称OoOE或OOE)是一种应用在高性能微处理器中来利用指令周期以免特定类型的延迟消耗的范式。

在这种范式中,处理器在一个由输入数据可用性所决定的顺序中执行指令,而不是由程序的原始数据所决定。在这种方式下,能够避免由于获取下一条程序指令所引发的处理器等待,取而代之的处理下一条能够当即执行的指令。 --维基百科

通俗地说,CPU内有多个计算单元,乱序执行的目的就是尽量多的地同时调动CPU内运算单元来提升运算效率(基于某种成熟算法),可是,编译器编译出来的代码并不是能达到这个效果,因此就须要CPU本身来调整代码执行顺序,举个例子,若是咱们要打开笔记本开始工做,流程大概是这样的:

拿出笔记本并按下开机键 -> 等待开机完成 -> 插上鼠标键盘 -> 打开软件开始修改bug

可是,若是遵守这个流程,在等待开机完成那段时间就是明显的浪费,因此,聪明的程序员通常会这样:

拿出笔记本并按下开机键 -> 等待开机完成 
                        插上鼠标键盘
                        回想昨天的bug,理清思路  -> 打开软件开始修改bug

这样,就明显节省了整个流程的时间。

CPU也能够是这样,尽管你的指令告诉它应该一步一步来,可是它以为有更高效的作法,并且还不用考虑人(机)道主义,若是能榨干它的最后一点价值,请尽管去作。

到这里咱们大概已经理解了CPU乱序执行的缘由,可是遗憾的是,对于CPU的乱序执行优化并不是有一个统一标准,由于更多地涉及到硬件,因此每每各厂商之间都有不一样的优化策略,优化效果也是不尽相同。

咱们看下面的例子:

a = 1
b = 1

上面简单的两条语句,在这样一种状况下会致使第二条比一条先执行:

操做a的时间须要等待的时间较长,而b不须要。比较一般的状况就是a在内存中,而b被缓存在寄存器或者高速缓存中且可用,对a发出读写指令后,须要等待。  

CPU检测这两条语句之间并无依赖性,能够调整执行顺序。  

先执行b = 1,再执行a = 1,以提升效率。

在单核的状况下这是没有问题的,主要是由于CPU能判断两条语句之间是否存在依赖关系,可是若是放在多核系统上就会出现麻烦,咱们来看看下面的例子:


在上述提到的状况中,CPU0乱序执行,先执行了b = 1,此时a还不等于1,因此在CPU1在检测到b!=1时,立马执行assert(a == 1),致使assert报错。

内存屏障

为了解决CPU乱序执行而带来的问题,内存屏障应运而生,许多CPU都提供内存屏障指令,内存屏障指令也是平台各异的,经典的X86下有ifence、mfence、sfence指令。

在Visual C++2005标准中,保证volatile提供一种内存屏障,组织编译器和CPU从新安排读入和写出。

PowerPC上则是lwsync。咱们把内存屏障指令插入不想被优化的指令以后便可达到相应的目的。

小结

在多线程中,为了解决多个线程同时操做同一份数据而带来同步问题,产生了原子操做,锁机制。

在缓存机制中,为了解决程序对数据的缓存而致使数据同步问题,增长了volatile关键词修饰。

在多核系统中,因为CPU乱序执行可能带来的问题,产生了内存屏障机制,以防止内存优化带来的问题。

参考:http://www.voidcn.com/article/p-fiewgxpd-bbh.html

《程序员的自我修养--连接、装载与库》

好了,关于原子操做、volatile、CPU执行顺序的讨论就到此为止啦,若是朋友们对于这个有什么疑问或者发现有文章中有什么错误,欢迎留言

原创博客,转载请注明出处!

祝各位早日实现项目丛中过,bug不沾身.

相关文章
相关标签/搜索