1. 原子操作的基础知识
sync/atomic 包实现了同步算法底层的原子内存操作原语,我们把它叫作原子操作原语,它提供了一些实现原子操作的函数和类型。它叫什么并不重要,重要的我们要熟悉它的功能。
之所以叫原子操作,是因为一个原子在执行的时候,其他线程不会看到执行一半的操作结果。在其他线程看来,原子操作要么执行完了,要么还没有执行,就像一个最小的粒子--原子一样,不可分割。
CPU 提供了基础的原子操作,不过,不同架构系统的原子操作是不一样的。对于单处理器单核系统来说,如果一个操作是由一个 CPU 指令实现的,比如 XCHG 和 INC 等指令,那么它就是原子操作。如果一个操作是基于多条指令实现的,那么它在执行的过程中可能会被中断,并执行上下文切换,这样的话,原子性的保证就被打破了,因为这个时候操作可能只执行了一半。
在多处理器多核系统中,原子操作的实现就比较复杂了。由于缓存的存在,单个核上的单个指令进行原子操作时,要确保其他处理器或 CPU 核不访问此原子操作的地址,或者确保其他处理器或CPU核总是访问原子操作之后的最新的值。 x86 架构中提供了指令前缀 LOCK, LOCK 保证了指令(如 LOCK CMPXCHG op1,op2) 不会受其他处理器或 CPU 核的影响,有些指令(如 XCHG)本身就提供了锁机制。不同的 CPU 架构提供的原子操作指令是不同的,比如对于多核的 MIPS 和 ARM ,提供了 LL/SC (Load Link/Store Conditional)指令,可以帮助实现原子操作。
因为不同的 CPU 架构甚至不同的版本提供的原子操作指令是不同的,所以要用一种编程语言实现支持不同架构的原子操作是相当有难度的。不过,这不需要我们操心,因为 Go 语言提供了一个通用的原子操作 API ,将底层不同架构下的实现封装成 atomic 包,以及提供了一个修改类型的原子操作(Read-Modify-Write,RMW) API 和一个加载存储类型的原子操作(Load 和 Store) API。
有的代码也会因为架构的不同而不同。有时候一个操作看起来是原子操作,但实际上,对于不同的架构来说,情况是不一样的。比如下面的代码,将一个 64 位的值赋给变量 i:
package main
const x int64 = 1 + 1<<33
func main() {
var i = x
_ = i
}
如果在 x386 架构下编译这段代码,var i=x 其实被拆分成两条指令,分别操作低 32 位和高 32 位的值。
注意: var i = x 是 int64 类型的赋值,在 x386 架构下它被编译成了两条 MOVL 指令,这就不是原子操作了。但是在 AMD64 架构下,编译的代码又是不同的,首先把常量赋给 R0 寄存器,然后真正通过 MOVD 指令一次赋值给 i 变量。