本文最后更新于 2024年10月15日 下午
概述 在日常开发中,不可避免的会碰到并发场景,在Go语言中处理同步的方法通常是使用锁,但如果是对单一的一个整数操作,这个时候使用锁可能会造成更大的性能开销,而且代码也失去了美观与优雅。 这个时候我们可以使用Go语言自带的原子操作,原子操作在Go语言的sync/atomic标准库里面,原子操作是比其他同步技术更基础的一种技术,而且原子操作是无锁的,通常是直接通过CPU指令实现。如果去看其他同步技术的源码可以看到很多技术都是依赖于原子操作的。
同步问题 在正式介绍原子操作之前先看一段代码,该代码中创建了100000个协程,对一个公共变量x进行累加操作,总共有3个版本的代码,第一个版本是普通版,第二个版本是加锁版本,第三个版本是原子操作版本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 var ( x int64 lock sync.Mutex wg sync.WaitGroup )func add () { x++ wg.Done() }func mutexAdd () { lock.Lock() x++ lock.Unlock() wg.Done() }func atomicAdd () { atomic.AddInt64(&x, 1 ) wg.Done() }func main () { start := time.Now() for i := 0 ; i < 100000 ; i++ { wg.Add(1 ) go atomicAdd() } wg.Wait() end := time.Now() fmt.Println("计算结果:" , x) fmt.Println("消耗时间:" , end.Sub(start)) }
依次运行三个版本,得出结果如下:
1 2 3 4 5 6 7 8 9 10 11 # 普通版本 计算结果: 96725 消耗时间: 26.4237 ms # 加锁版本 计算结果: 100000 消耗时间: 31.2588 ms # 原子操作版本 计算结果: 100000 消耗时间: 27.3615 ms
从上面的结果可以看出,普通版本的直接结算结果就是错误的,这个因为普通版本的不是并发安全的,所以会导致计算错误。加锁版本和原子操作版本都是计算正确,但是原子操作版本所消耗时间要比加锁版本更低(如果数字更大相差时间可能会更多,可以自行尝试)。
atomic 所有的原子操作都在atomic包下面,对于int32,int64,uint32,uint64,uintptr和Pointer类型,都有其对应的原子操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 func SwapInt32 (addr *int32 , new int32 ) (old int32 )func SwapInt64 (addr *int64 , new int64 ) (old int64 )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 SwapPointer (addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)func CompareAndSwapInt32 (addr *int32 , old, new int32 ) (swapped bool )func CompareAndSwapInt64 (addr *int64 , old, new int64 ) (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 CompareAndSwapPointer (addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool )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 )func LoadInt32 (addr *int32 ) (val int32 )func LoadInt64 (addr *int64 ) (val int64 )func LoadUint32 (addr *uint32 ) (val uint32 )func LoadUint64 (addr *uint64 ) (val uint64 )func LoadUintptr (addr *uintptr ) (val uintptr )func LoadPointer (addr *unsafe.Pointer) (val unsafe.Pointer)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)
以上是atomic包中的所有方法,主要分为5种类型,下面根据不同类型逐一讲解。
Load和Store Load和Store方法主要用来在并发环境下实现对数字的设置和读取,Store表示给变量设置一个值,Load表示读取变量的值。
1 2 3 4 5 6 7 var value int64 func main () { atomic.StoreInt64(&value, 1 ) val := atomic.LoadInt64(&value) fmt.Println("value: " , val) }
Add Add方法更简单,就是给一个变量加上一个值,使用Add方法加是并发安全的,不会出现上面示例中普通版本的add函数一样出现计算错误的问题。如果变量是有符号整数类型,需要实现对变量的减法,只需要调用Add方法的时候第二个参数传入负数即可。
Swap和CompareAndSwap Swap是交换的意思,使用Swap方法可以修改变量的值,同时会将变量的旧值返回。 CompareAndSwap是比较并交换的意思,作用与Swap类似,也是修改变量的值,但是在调用CompareAndSwap的时候需要传入需要设置的新值和期望的旧值,如果当前变量的值和期望的旧值一样,才会将变量修改会新值,同时返回是否修改成功。
1 2 3 4 5 6 7 8 9 10 var value int64 = 1 func main () { old := atomic.SwapInt64(&value, 2 ) fmt.Printf("旧值:%d, value:%d\n" , old, value) swapped := atomic.CompareAndSwapInt64(&value, 1 , 3 ) fmt.Printf("修改结果:%t, value: %d\n" , swapped, value) swapped = atomic.CompareAndSwapInt64(&value, 2 , 3 ) fmt.Printf("修改结果:%t, value: %d\n" , swapped, value) }
上面的示例中将value设置为1,先使用Swap方法将Value修改为2,同时返回修改前的值。再使用CompareAndSwap想要修改为3,但是因为传入的期望值1和value的实际值2不相等,所以修改失败,再次调用期望值为2且value的实际值为2,则修改成功。运行结果如下:
1 2 3 旧值:1 , value:2 修改结果:false , value: 2 修改结果:true , value: 3
新版本结构体类型 在1.19版本,Go语言在atomic中新增了Int32,Int64等结构体类型,使用结构体类型进行原子操作更简单,不需要再想之前一样每次从atomic中调用各种类型的方法来实现原子操作。而是只需要使用结构体的方法即可直接进行原子操作。
1 2 3 4 5 6 7 8 9 10 11 12 var value atomic.Int64func main () { value.Store(1 ) fmt.Println("value: " , value.Load()) n := value.Add(1 ) fmt.Println("value: " , n) old := value.Swap(3 ) fmt.Printf("旧值:%d, value:%d\n" , old, value.Load()) swapped := value.CompareAndSwap(3 , 4 ) fmt.Printf("修改结果:%t, value:%d\n" , swapped, value.Load()) }
上面示例中使用的就是最新的写法结构体类型,运行结果如下:
1 2 3 4 value: 1 value: 2 旧值:2 , value:3 修改结果:true , value:4