go语法:sync/atomaic原子操作
参考:
https://www.jianshu.com/p/6ca885ede2a8(sync/atomaic原子操作)
https://zhuanlan.zhihu.com/p/401606797(知乎:atomic原子操作)
核心概念:
原子性:一个或多个操作在CPU的执行过程中不被中断的特性,称为原子性。这些操作对外表现成一个不可分割的整体,他们要么都执行,要么都不执行,外界不会看到他们只执行到一半的状态。
原子操作:进行过程中不能被中断的操作,原子操作由底层硬件支持,而锁则是由操作系统提供的API实现,若实现相同的功能,前者通常会更有效率
协程并发问题
sync.WaitGroup
包保证我们创建的1000个goroutine全部执行完毕后再输出n
,运行程序看看结果如何:func no_atomic() { var n int32 var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func() { n++ wg.Done() }() } wg.Wait() fmt.Println(atomic.LoadInt32(&n)) // 978 }
电脑多核的情况下,不限制cpu执行上面代码,每次执行的结果不是1000,而是小于等于一千数值波动。为什么会出现这种情况呢?上面我们通过for循环里创建1000个goroutine,每个goroutine都将n加上1,加入有其中两个goroutine如下执行步骤:
- goroutine1读取变量n 值为800
- goroutine2读取变量n 值为800
- goroutine1执行n+1,n变为801
- goroutine2执行n+1,n变为801
- 两次goroutine执行完毕
- 结果n少加了一次1,所以最终的结果就比预期少1
sync/atomic的原子操作,主要有五大类:
Load:返回原值
1,该类方法主要负责从相应的内存地址中获取对应的值
2,Load 方法是为了防止在读取过程中,有其他协程发起修改动作,影响了读取结果,常用于配置项的整个读取。
3,读取的时候,其他协程不能写入
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)
Store:无返回值
1,整体赋值
2,有原子读取,就有原子修改值,前面提到过的 Add 只适用于 int、uint 类型的增减,并没有其他类型的修改,而 Sotre 方法通过 unsafe.Pointer 指针原子修改,来达到了对其他类型的修改。
3,写入的时候,其他协程不能读取
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)
Add:返回新值
1,原子增减
2,可以理解为 先load,再+=,再store,三者中间不会打断
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)
Swap:返回旧值
1,原子赋值
2,可以理解为 先load旧值,再Store新值,然后返回旧值
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)
CompareAndSwap:返回bool值
1,该操作在进行交换前首先确保被操作数的值未被更改,即仍然保存着参数 old 所记录的值,满足此前提条件下才进行交换操作。
2,CAS的做法类似操作数据库时常见的乐观锁机制。
3,当有大量的goroutine 对变量进行读写操作时,可能导致CAS操作无法成功,这时可以利用for循环多次尝试。
4,CompareAndSwap 有可能产生 ABA 现象发生。也就是原来的值是 A,后面被修改 B,再后面修改为 A。在这种情况下也符合了 CompareAndSwap 规则,即使中途有被改动过。
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)
atomic包中支持六种类型
int32
uint32
int64
uint64
uintptr
unsafe.Pointer
对于每一种类型,提供了五类原子操作:
LoadXXX(addr): 原子性的获取*addr的值,等价于:
return *addr
StoreXXX(addr, val): 原子性的将val的值保存到*addr,等价于:
addr = val
AddXXX(addr, delta): 原子性的将delta的值添加到*addr并返回新值(unsafe.Pointer不支持),等价于:
*addr += delta return *addr
SwapXXX(addr, new) old: 原子性的将new的值保存到*addr并返回旧值,等价于:
old = *addr *addr = new return old
CompareAndSwapXXX(addr, old, new) bool: 原子性的比较*addr和old,如果相同则将new赋值给*addr并返回true,等价于:
if *addr == old { *addr = new return true } return false
使用sync/atomic解决文首的问题,代码如下:
func _atomic() {
var n int32
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
atomic.AddInt32(&n, 1)
wg.Done()
}()
}
wg.Wait()
fmt.Println(atomic.LoadInt32(&n)) // 1000
}
atomic.AddInt32
进行加操作,最终输出预期的结果, 其实atomic包中的方法在执行完毕之前不会被其他的任务或者事件中断,该操作是并发安全,在向此地址写入或者读取值时原子性的操作,各类方法的应用场景,在实际应用中发掘和体会