本篇文章将以Intel CPU作为讨论基础

一、并发的由来

  1. 一台计算机有2个cpu,其中CPU1履行程序A,CPU2履行程序B,因为程序A和程序B是两个不同的运用程序,所以它们两个之间并不存在并发问题。

并发原理 — CPU原子性指令(一)

2.现在两个CPU要履行程序A的同一段代码,比方对变量a履行加1操作,代码a=a+1经过汇编器编译之后便是如下指令片段:addl $1,-4(%esp);

并发原理 — CPU原子性指令(一)

Intel CPU的指令集属于杂乱指令集类型(CISC),其内部也是由微指令组成,就拿上面的加1操作指令,在CPU内部履行的时分可能会分成如下三步:

(1)从内存中取到1放入寄存器中

movl -4(%ebp),%eax

(2)对寄存器中的数履行加1操作

addl $1,%eax

(3)将加操作的成果写回内存中

movl %eax,-4(%ebp)

3.那么问题就来了:因为履行的是同一段代码,咱们期望最后的成果是两个CPU各加一次,也便是a=3。但实际情况存在交叉履行:CPU1和CPU2同时履行了步骤1,此刻a=1;然后CPU2履行了加法操作并把数据写入了内存,此刻a=2,但CPU1现已取到的值是1,履行加法指令操作,接着把成果写入内存,成果依然是a=2。与咱们预期的a=3不符,形成数据紊乱。形成数据紊乱的根本原因便是交叉履行,多个CPU操作同一个可读可写的数据就会有并发问题。那在CPU层面是怎么处理并发问题呢?

并发原理 — CPU原子性指令(一)

二、CPU处理并发问题的方案

1.对总线上锁

为了处理并发问题,咱们能想到的便是把以上三个步骤的代码当作一个整体,要么悉数不履行,要么悉数履行不能被交叉,咱们称之为原子性代码:将一串操作定义为不能拆散的基本操作。

CPU在访存的时分需要通过控制总线、数据总线和地址总线相结合从内存读写数据,由此能够得知第一种处理办法便是对总线上锁。但这种办法有个很大的坏处,一旦锁了总线,就算其他CPU履行的不是同一个程序的代码,那么这些CPU也得等候,这将形成严重的性能损耗,锁的粒度太大,为此咱们考虑怎么将锁细粒度化?

并发原理 — CPU原子性指令(一)

2.对缓存行上锁

CPU内部有缓存,为了加快拜访,CPU会把数据a相邻连续的一段内存空间数据(缓存行)加载到缓存中,为此得到了咱们第二种处理数据紊乱的办法:对缓存行上锁。一旦数据a发生改变,会根据CPU的MESI协议来更新数据,达到数据一致性。[关于CPU的MESI协议能够参阅本篇文章]

并发原理 — CPU原子性指令(一)

3.原子性指令

综上所述,intel cpu供给了原子性指令,只要指令支持lock前缀,那么它就能够把这条指令变成原语指令(原语:原子性的语义),比方 add,加上lock前缀便是 lock;add ,那么加法操作就变成了原子性操作,不会再被交叉履行了。但也有一些指令比较特殊,它们本身便是原语指令,不需要lock前缀,比方:xchg。

三、上层运用的并发处理方案

只有CPU供给了原子性指令,上层运用才能够根据这些指令来规划出指令段与指令段之间的原子性操作。这是一种自底向上的规划,没有CPU最底层的支持,上层运用根本就无法处理并发问题。运用程序运用本身语言供给的并发操作函数库,比方java的juc包,而这些函数库又会封装OS的体系调用或许运用glibc库,OS的体系调用最终会运用CPU供给的原子性指令。

并发原理 — CPU原子性指令(一)

本文运用 文章同步帮手 同步