原子操作是指一个原子在执行的时候, 其他线程不会看到执行一半的操作的结果. 在其他线程看来, 原子操作要么执行完了, 要么还没执行, 就像一个最小的粒子-原子一样, 不可分割
CPU 提供了基础的原子操作, 不过, 不同架构系统的原子操作是不一样的, 对于单处理器单核系统来说,如果一个操作是由一个 CPU 指令实现的, 比如 XCHG 和 INC 等指令, 那么它就是原子操作. 如果一个操作的基于多条指令实现的, 那么它在执行的过程中可能会被中断, 并执行上下文切换, 这样的话, 原子性的保证就被打破, 因为这个时候操作可能执行了一半.
在多处理器多核系统中, 原子操作的实现就比较复杂了. 由于缓存的存在, 单个核上的单个指令进行原子操作时, 要确保其他处理器或 CPU 核不访问此原子操作的地址, 或者确保其他处理器或 CPU 核总是访问原子操作之后的最新的值.
如 x86 架构中提供了指令前缀 LOCK, LOCK 保证了指令(如 LOCK CMPXCHG op1, op2) 不会受其他处理器或CPU核的影响, 有些指令(如 XCHG) 本身就提供了锁机制. 对于多核的 MIPS 和 ARM, 提供了 LL/SC(Load Link/Store Conditional) 指令, 可以帮助实现原子操作
Go 语言提供一个通用的原子操作API, 将底层不同架构下的实现封装成 atomic 包, 以及提供了一个修改类型的原子操作(Read-Modify-Write RMW) API 和一个加载存储类型的原子操作(Load 和 Store) API.
atomic 操作的对象是一个地址, 需要把可寻址的变量的地址作为参数传递给函数, 而不是把变量的值传递给函数
AddXXX 函数就是给第一个参数地址中的值增加一个 delta 值, 也就是原子地给一个值增加或减去一个变化值
对于有符号整数来说, delta 值可以是一个正数, 代表增加一个值; 也可以是一个负数, 代表减去一个值.
对于无符号的整数和 uinptr 类型来说, 实现减去一个值, 可以采用计算机原理中补码原理, 变减法为加法
// 函数签名
func AddInt32(addr *int32, delta int32) (new int32)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)
// 补码原理实现无符号减去一个值
AddUint32(&x, ^uint32*(c-1))
func Run() {
var x uint64 = 0
newXValue := atomic.AddUint64(&x, 100)
fmt.Println(newXValue) // 100
newXValue = atomic.AddUint64(&x, ^uint64(0))
fmt.Println(newXValue) // 99
newXValue = atomic.AddUint64(&x, ^uint64(10-1))
fmt.Println(newXValue) // 89
}
CompareAndSwapXXX 函数需要提供操作地址, 旧值, 新值, 会比较当前 addr 地址中的值与 old 是否相等, 如果不相等, 则返回 false; 如果相等, 则把此地址的值替换成 new , 返回 true
if *addr == old {
*addr = new
return true
}
return false
// 函数签名
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
func Run1() {
var x uint64 = 0
ok := atomic.CompareAndSwapUint64(&x, 0, 100)
fmt.Println(ok) // true
ok = atomic.CompareAndSwapUint64(&x, 0, 100)
fmt.Println(ok) // false
}
如果不需要比较旧值, 直接替换的话则可以使用 SwapXXX 函数
old = *addr
*add = new
return old
func SwapInt32(addr *int32, new int32) (old int32)
func SwapInt64(addr *int64, new int64) (old int64)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)
func SwapUint32(addr *uint32, new uint32) (old uint32)
func SwapUint64(addr *uint64, new uint64) (old uint64)
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
func Run2() {
var x uint64 = 0
old := atomic.SwapUint64(&x, 100)
fmt.Println(old) // 0
old = atomic.SwapUint64(&x, 100)
fmt.Println(old) // 100
}
LoadXXX 函数会取出 addr 地址中的, 即使在多处理器, 多核, 有 CPU 缓存情况下也能保证 Load 是一个原子操作
func LoadInt32(addr *int32) (val int32)
func LoadInt64(addr *int64) (val int64)
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
func LoadUint32(addr *uint32) (val uint32)
func LoadUint64(addr *uint64) (val uint64)
func LoadUintptr(addr *uintptr) (val uintptr)
func Run3() {
var x uint64 = 0
v := atomic.LoadUint64(&x)
fmt.Println(v) // 0
x = 100
v = atomic.LoadUint64(&x)
fmt.Println(v) // 100
}
StoreXXX 函数会把一个字存入指定的 addr 地址中, 即使在多处理器, 多核, 有 CPU 缓存情况下也能保证 Store 是一个原子操作. 其他的 goroutine 通过 LoadXXX 函数存取值时, 不会看到只存取一半的值.
func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintptr, val uintptr)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
func Run4() {
var x uint64 = 0
atomic.StoreUint64(&x, 100)
fmt.Println(x)
}
atomic 提供一些特殊/包装类型, 如 Value, Bool, Int32, Int64, Pointer, Uint32, Uint64 和 Uintptr, 其实现是调用相对应的原子操作函数.