CAS
CAS全称Compare And Swap,是一种硬件级别的原子操作.
golang中的atomic包的CompareAndSwapXXX函数,针对不同类型的变量提供CAS操作,这些函数都需要提供3个值分别为
- 地址: 要修改值的地址
- 旧值: 原来的值
- 新值: 要更新的值
这些函数的实现都是在汇编代码中.
TEXT ·CompareAndSwapUint64(SB),NOSPLIT,$0-25
MOVQ addr+0(FP), BP
MOVQ old+8(FP), AX
MOVQ new+16(FP), CX
LOCK
CMPXCHGQ CX, 0(BP)
SETEQ swapped+24(FP)
RET
比较eax和目的操作数(第一个操作数)的值,如果相同,ZF标志被设置,同时源操作数(第二个操作)的值被写到目的操作数,否则,清除ZF标志,并且把目的操作数的值写回eax。
CMPXCHG 的工作原理:
比较 _old 和 (*__ptr) 的值,如果相同,ZF 标志被设置,同时 _new 的值被写到 (*__ptr),否则,清 ZF 标志,并且把 (*__ptr) 的值写回 _old。
在 Intel 平台下,会用 LOCK CMPXCHG 来实现,这里的 LOCK 是 CPU 锁。
Intel 的手册对 LOCK 前缀的说明如下:
- 1.确保对内存的读-改-写操作原子执行。在 Pentium 及 Pentium 之前的处理器中,带有 LOCK 前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从 Pentium 4,Intel Xeon 及 P6 处理器开始,Intel 在原有总线锁的基础上做了一个很有意义的优化:如果要访问的内存区域(area of memory)在 LOCK 前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),并且该内存区域被完全包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性。这个操作过程叫做缓存锁定(cache locking),缓存锁定将大大降低 LOCK 前缀指令的执行开销,但是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。
- 2.禁止该指令与之前和之后的读和写指令重排序。
- 3.把写缓冲区中的所有数据刷新到内存中。
CPU 锁主要分两种,总线锁和缓存锁。
总线锁就是使用 CPU 提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该 CPU 可以独占使用共享内存。总线锁的这种方式,在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。所以总线锁定的开销比较大,最新的处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。
缓存锁定就是如果缓存在处理器缓存行中内存区域在 LOCK 操作期间被锁定,当它执行锁操作回写内存时,处理器不在总线上产生LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时会对缓存行无效。
处理器无法使用缓存锁的情况。
- 当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line),则处理器会调用总线锁定。
- 有些处理器不支持缓存锁定。一些老的 CPU 就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。
虽然缓存锁可以大大降低 CPU 锁的执行开销,但是如果遇到多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。所以缓存锁常与总线锁相互配合。