一、原子操作
原子操作是变量级别的互斥锁。
同一时刻,只能有一个 CPU 对变量进行读或写。 当我们想要对某个变量做并发安全的修改,除了使用官方提供的 Mutex
,还可以使用 sync/atomic
包的原子操作,可以把 sync/atomic
包中的原子操作看成是变量级别的互斥锁,它能够保证对变量的读取或修改期间不被其他的协程所影响。
二、mutex与atomic
在 Go(甚至是大部分语言)中,一条普通的赋值语句其实并不是一个原子操作(语言规范同样没有定义 i++
是原子操作, 任何变量的赋值都不是原子操作)。例如,在 32 位机器上写 int64
类型的变量是有中间状态的,它会被拆成两次写操作 MOV
—— 写低 32 位和写高 32 位
一种解决方法就是上锁,另一种就是利用 atomic
特性。
那 atomic
会比 mutex
有什么好处呢:mutex
由操作系统实现,而 atomic
包中的原子操作则由底层硬件直接提供支持。在 CPU 实现的指令集里,有一些指令被封装进了 atomic
包,这些指令在执行的过程中是不允许中断(interrupt)的,因此原子操作可以在 lock-free 的情况下保证并发安全,并且它的性能也能做到随 CPU 个数的增多而线性扩展。
对于现代的多处理多核的系统来说,一个核对地址的值的更改,在更新到主内存中之前,存放在在多级缓存中。这时候,其它的核看的的可能是还没有更新的数据。
多核处理器为了解决这种问题,使用了一种内存屏障的方式。使用这种方式后,数据的读写机制有点类似读写锁,并且写操作还会让 CPU 缓存失效,以便其它核能从主内存中拉取最新的值。
atomic 包提供的方法会提供内存屏障的功能,所以,atomic 不仅仅可以保证赋值的数据完整性,还能保证数据的可见性,一旦一个核更新了该地址的值,其它处理器总是能读取到它的最新值。
若实现相同的功能,atomic
通常会更有效率,并且更能利用计算机多核的优势。所以,以后当我们想并发安全的更新一些变量的时候,我们应该优先选择用 atomic
来实现。
二、原子操作的使用场景是什么
原子操作本质上是一种变量级别的互斥锁。 因此,原子操作的使用场景也是和互斥锁类似的,但是不一样的是,我们的锁粒度只是一个变量而已。 也就是说,当我们不允许多个 CPU 同时对变量进行读写的时候(保证变量同一时刻只能一个 CPU 操作),就可以使用原子操作。
单核情况下,即使一个操作翻译成汇编不止一个指令,也有可能保持一致性。比如经常用来演示的并发场景下的 count++ 操作 (count++ 对应的汇编指令就有三条),如果像下面这样,无论执行多少次,输出结果都是 2000。
func main() {
runtime.GOMAXPROCS(1)
var w sync.WaitGroup
count := int32(0)
w.Add(100)
for i := 0; i < 100; i++ {
go func() {
for j := 0; j < 20; j++ {
count++
}
w.Done()
}()
}
w.Wait()
fmt.Println(count)
}
而在多核情况中,A核修改 count 的时候,由于 CPU 缓存的存在,B核读到的 count 值可能不是最新的值。如果我们将上面的例子中的第二行改成:
runtime.GOMAXPROCS(2)
之后,程序每执行一次,结果都有可能不一样。
解决思路除了使用 Mutex 加锁,也可以使用 atomic,具体使用方法是将 count++ 替换成:
atomic.AddInt32(&count, 1)
这样就能保证即使在多核情况下 count++ 也是一个原子操作。
三、 atomic 基本方法
3.1、增减 Add
- 用于进行增加或减少的原子操作,函数名以
Add
为前缀,后缀针对特定类型的名称。 - 原子增被操作的类型只能是数值类型,即
int32
、int64
、uint32
、uint64
、uintptr
- 原子增减函数的第一个参数为原值,第二个参数是要增减多少。
int32
和int64
的第二个参数可以是负数,这样就可以做原子减法了。
func AddInt32(addr *int32, delta int32) (new int32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)
3.2、交换 Swap
Swap
跟 Store
有点类似,但是它会返回 *addr
的旧值
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)
3.3、 比较并交换 CAS(CompareAndSwap)
在 CAS
操作中,会需要拿 new
的值跟 old
比较,如果相等,就将 new
赋值给 addr
。 如果不相等,则不做任何操作。最后返回一个 bool
值,表示是否成功 swap
。
CompareAndSwap
的功能:
- 用于比较并交换的原子操作,函数名以
CompareAndSwap
为前缀,后缀针对特定类型的名称。 - 原子比较并交换被操作的类型可以是数值类型或指针类型,即
int32
、int64
、uint32
、uint64
、uintptr
、unsafe.Pointer
- 原子比较并交换函数的第一个参数为原值指针,第二个参数是要比较的值,第三个参数是要交换的值。
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)
CAS最大的一个缺点就是:CAS 有自旋锁,如果不成功会一直循环,可能会给 cpu 带来很大开销。
3.4、 载入 Load
Load 方法会取出 addr 地址中的值,意味着读取值的同时,当前计算机的任何 CPU 都不会进行针对值的读写操作,即使在多处理器、多核、有 CPU cache 的情况下,Load 方法能保证数据的读一致性。
如果不使用原子 Load
,当使用 v := value
这种赋值方式为变量 v
赋值时,读取到的 value
可能不是最新的,因为在读取操作时其他协程对它的读写操作可能会同时发生。
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)
3.5、存储 Store
Store
可以将 val
值保存到 *addr
中,Store
操作是原子性的,因此在执行 Store
操作时,当前计算机的任何 CPU 都不会进行针对 *addr
的读写操作,其它协程通过 Load 读取数据,不会看到存取了一半的值
func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintptr, val uintptr)
3.6、 原子操作任意类型的值 Value类型
上面的几种方法只支持基本的几种类型,因此,atomic 还提供了一个 Value 类型,它可以实现对任意类型(结构体)原子的存取操作,相比于上面的 StoreXXX 和 LoadXXX,value的操作效率会低一些,不过胜在简单易用。
// A Value provides an atomic load and store of a consistently typed value.
// The zero value for a Value returns nil from Load.
// Once Store has been called, a Value must not be copied.
//
// A Value must not be copied after first use.
type Value struct {
v interface{}
}
func (v *Value) Load() (x interface{})
func (v *Value) Store(x interface{})