详解Golang五种原⼦性操作的⽤法
⽬录
Go 语⾔提供了哪些原⼦操作
互斥锁跟原⼦操作的区别
⽐较并交换
atomic.Value保证任意值的读写安全
总结
本⽂我们详细聊⼀下Go语⾔的原⼦操作的⽤法,啥是原⼦操作呢?顾名思义,原⼦操作就是具备原⼦性的操作... 是不是感觉说了跟没说⼀样,原⼦性的解释如下:
⼀个或者多个操作在 CPU 执⾏的过程中不被中断的特性,称为原⼦性(atomicity)。这些操作对外表现成⼀个不可分割的整体,他们要么都执⾏,要么都不执⾏,外界不会看到他们只执⾏到⼀半的状态。
CPU执⾏⼀系列操作时不可能不发⽣中断,但如果我们在执⾏多个操作时,能让他们的中间状态对外不可
见,那我们就可以宣称他们拥有了"不可分割”的原⼦性。
类似的解释我们在数据库事务的ACID概念⾥也听过,只不过这⾥保障原⼦性的执⾏体是CPU。
Go 语⾔提供了哪些原⼦操作
Go语⾔通过内置包sync/atomic提供了对原⼦操作的⽀持,其提供的原⼦操作有以下⼏⼤类:
增减,操作⽅法的命名⽅式为AddXXXType,保证对操作数进⾏原⼦的增减,⽀持的类型为int32、int64、uint32、uint64、uintptr,使⽤时以实际类型替换前⾯我说的XXXType就是对应的操作⽅法。
载⼊,保证了读取到操作数前没有其他任务对它进⾏变更,操作⽅法的命名⽅式为LoadXXXType,⽀持的类型除了基础类型外还⽀持Pointer,也就是⽀持载⼊任何类型的指针。
存储,有载⼊了就必然有存储操作,这类操作的⽅法名以Store开头,⽀持的类型跟载⼊操作⽀持的那些⼀样。
⽐较并交换,也就是CAS (Compare And Swap),像Go的很多并发原语实现就是依赖的CAS操作,同样是⽀持上⾯列的那些类型。
交换,这个简单粗暴⼀些,不⽐较直接交换,这个操作很少会⽤。
互斥锁跟原⼦操作的区别
平⽇⾥,在并发编程⾥,Go语⾔sync包⾥的同步原语Mutex是我们经常⽤来保证并发安全的,那么他跟atomic包⾥的这些操作有啥区别呢?在我看来他们在使⽤⽬的和底层实现上都不⼀样:
使⽤⽬的:互斥锁是⽤来保护⼀段逻辑,原⼦操作⽤于对⼀个变量的更新保护。
底层实现:Mutex由操作系统的调度器实现,⽽atomic包中的原⼦操作则由底层硬件指令直接提供⽀持,这些指令在执⾏的过程中是不允许中断的,因此原⼦操作可以在lock-free的情况下保证并发安全,并且它的性能也能做到随CPU个数的增多⽽线性扩展。
对于⼀个变量更新的保护,原⼦操作通常会更有效率,并且更能利⽤计算机多核的优势。
⽐如下⾯这个,使⽤互斥锁的并发计数器程序:
func mutexAdd() {
var a int32 = 0
var wg sync.WaitGroup
var mu sync.Mutex
start := time.Now()
for i := 0; i < 100000000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
a += 1
mu.Unlock()
}()
}
wg.Wait()
timeSpends := time.Now().Sub(start).Nanoseconds()
fmt.Printf("use mutex a is %d, spend time: %v\n", a, timeSpends)
}
把Mutex改成⽤⽅法atomic.AddInt32(&a, 1)调⽤,在不加锁的情况下仍然能确保对变量递增的并发安全。
func AtomicAdd() {
var a int32 = 0
var wg sync.WaitGroup
start := time.Now()
for i := 0; i < 1000000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt32(&a, 1)
go语言入门书籍}()
}
wg.Wait()
timeSpends := time.Now().Sub(start).Nanoseconds()
fmt.Printf("use atomic a is %d, spend time: %v\n", atomic.LoadInt32(&a), timeSpends)
}
可以在本地运⾏以上这两段代码,可以观察到计数器的结果都最后都是1000000,都是线程安全的。
需要注意的是,所有原⼦操作⽅法的被操作数形参必须是指针类型,通过指针变量可以获取被操作数在内存中的地址,从⽽施加特殊的CPU指令,确保同⼀时间只有⼀个goroutine能够进⾏操作。
上⾯的例⼦除了增加操作外我们还演⽰了载⼊操作,接下来我们来看⼀下CAS操作。
⽐较并交换
该操作简称CAS (Compare And Swap)。这类操作的前缀为 CompareAndSwap :
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
该操作在进⾏交换前⾸先确保被操作数的值未被更改,即仍然保存着参数 old 所记录的值,满⾜此前提条件下才进⾏交换操作。CAS的做法类似操作数据库时常见的乐观锁机制。
需要注意的是,当有⼤量的goroutine 对变量进⾏读写操作时,可能导致CAS操作⽆法成功,这时可以利⽤for循环多次尝试。
上⾯我只列出了⽐较典型的int32和unsafe.Pointer类型的CAS⽅法,主要是想说除了读数值类型进⾏⽐较交换,还⽀持对指针进⾏⽐较交换。
unsafe.Pointer提供了绕过Go语⾔指针类型限制的⽅法,unsafe指的并不是说不安全,⽽是说官⽅并不保证向后兼容。
// 定义⼀个struct类型P
type P struct{ x, y, z int }
// 执⾏类型P的指针
var pP *P
func main() {
// 定义⼀个执⾏unsafe.Pointer值的指针变量
var unsafe1 = (*unsafe.Pointer)(unsafe.Pointer(&pP))
// Old pointer
var sy P
// 为了演⽰效果先将unsafe1设置成Old Pointer
px := atomic.SwapPointer(
unsafe1, unsafe.Pointer(&sy))
// 执⾏CAS操作,交换成功,结果返回true
y := atomic.CompareAndSwapPointer(
unsafe1, unsafe.Pointer(&sy), px)
fmt.Println(y)
}
上⾯的⽰例并不是在并发环境下进⾏的CAS,只是为了演⽰效果,先把被操作数设置成了Old Pointer。
其实Mutex的底层实现也是依赖原⼦操作中的CAS实现的,原⼦操作的atomic包相当于是sync包⾥的那些同步原语的实现依赖。
⽐如互斥锁Mutex的结构⾥有⼀个state字段,其是表⽰锁状态的状态位。
type Mutex struct {
state int32
sema uint32
}
为了⽅便理解,我们在这⾥将它的状态定义为0和1,0代表⽬前该锁空闲,1代表已被加锁,以下是sync.Mutex中Lock⽅法的部分实现代码。
func (m *Mutex) Lock() {
// Fast path: grab unlocked mutex.
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}
// Slow path (outlined so that the fast path can be inlined)
m.lockSlow()
}
在atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked)中,m.state代表锁的状态,通过CAS⽅法,判断锁此时的状态是否空闲(m.state==0),是,则对其加锁(mutexLocked常量的值为1)。
atomic.Value保证任意值的读写安全
atomic包⾥提供了⼀套Store开头的⽅法,⽤来保证各种类型变量的并发写安全,避免其他操作读到了修改变量过程中的脏数据。
func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
...
这些操作⽅法的定义与上⾯介绍的那些操作的⽅法类似,我就不再演⽰怎么使⽤这些⽅法了。
值得⼀提的是如果你想要并发安全的设置⼀个结构体的多个字段,除了把结构体转换为指针,通过StorePointer设置外,还可以使⽤atomic包后来引⼊的atomic.Value,它在底层为我们完成了从具体指针类型到unsafe.Pointer之间的转换。
有了atomic.Value后,它使得我们可以不依赖于不保证兼容性的unsafe.Pointer类型,同时⼜能将任意数据类型的读写操作封装成原⼦性操作(中间状态对外不可见)。
atomic.Value类型对外暴露了两个⽅法:
v.Store(c) - 写操作,将原始的变量c存放到⼀个atomic.Value类型的v⾥。
c := v.Load() - 读操作,从线程安全的v中读取上⼀步存放的内容。
1.17 版本我看还增加了Swap和CompareAndSwap⽅法。
简洁的接⼝使得它的使⽤也很简单,只需将需要做并发保护的变量读取和赋值操作⽤Load()和Store()代替就⾏了。
由于Load()返回的是⼀个interface{}类型,所以在使⽤前我们记得要先转换成具体类型的值,再使⽤。下⾯是⼀个简单的
例⼦演⽰atomic.Value的⽤法。
type Rectangle struct {
length int
width int
}
var rect atomic.Value
func update(width, length int) {
rectLocal := new(Rectangle)
rectLocal.width = width
rectLocal.length = length
rect.Store(rectLocal)
}
func main() {
wg := sync.WaitGroup{}
wg.Add(10)
// 10 个协程并发更新
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
update(i, i+5)
}()
}
wg.Wait()
_r := rect.Load().(*Rectangle)
fmt.Printf("rect.width=%d\nrect.length=%d\n", _r.width, _r.length)
}
你也可以试试,不⽤atomic.Value,直接给Rectange类型的指针变量赋值,看看在并发条件下,两个字段的值是不是能跟预期的⼀样变成10和15。
总结
本⽂详细介绍了Go语⾔原⼦操作atomic包中会被⾼频使⽤的操作的使⽤场景和⽤法,当然我并没有罗列atomic包⾥所有操作的⽤法,主要是考虑到有的⽤到的地⽅实在不多,或者是已经被更好的⽅式替代,还有就是觉得确实没必要,看完本⽂的内容相信你已经完全具备⾃⾏探索atomic包的能⼒了。
再强调⼀遍,原⼦操作由底层硬件⽀持,⽽锁则由操作系统的调度器实现。锁应当⽤来保护⼀段逻辑,
对于⼀个变量更新的保护,原⼦操作通常会更有效率,并且更能利⽤计算机多核的优势,如果要更新的是⼀个复合对象,则应当使⽤atomic.Value封装好的实现。
到此这篇关于详解Golang五种原⼦性操作的⽤法的⽂章就介绍到这了,更多相关详解Golang五种原⼦性操作的⽤法内容请搜索以前的⽂章或继续浏览下⾯的相关⽂章希望⼤家以后多多⽀持!
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论