原子操作
什么是原子操作
原子操作(atomic operation)是指不会被线程调度机制打断的一个或一组操作。原子操作是不可分割的,在执行完毕之前不会被任何其它任务或事件中断。和锁相比原子操作的优点是相对较快,并且不会有死锁问题,缺点是它仅仅能执行有限的一组操作,并且通常这些操作还不能以有效地合成更复杂的操作。
golang有专门的一个package sync/atomic
来实现原子操作的相关功能。
原子增减
golang 对一个数值类型进行原子性增减。
有如下几个api函数
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)
package main
import (
"fmt"
"sync/atomic"
"time"
)
func main(){
var a int32= 0
for i:=0;i<100;i++ {
go func() {
atomic.AddInt32(&a,1) //这边是+1,如果要减一 那就第二个参数就是 -1
}()
}
time.Sleep(1e9)
fmt.Println(a)
a = 0
for i:=0;i<100;i++ {
go func() {
a ++ //这边的操作非原子性 所以结果可能不是等于100,这边顺带说明 a++ 是不具有原子性的
}()
}
time.Sleep(1e9)
fmt.Println(a)
}
$ go run main.go
100
94
我们看下汇编
$ go build -gcflags -S 2>&1 | grep ".go:12" |grep -v "PCDATA"
0x0000 00000 (/home/wida/gocode/atomic-demo/main.go:12) MOVL $1, AX
0x0005 00005 (/home/wida/gocode/atomic-demo/main.go:12) MOVQ "".&a+8(SP), CX
0x000a 00010 (/home/wida/gocode/atomic-demo/main.go:12) LOCK
0x000b 00011 (/home/wida/gocode/atomic-demo/main.go:12) XADDL AX, (CX)
$ go build -gcflags -S 2>&1 | grep ".go:21" |grep -v "PCDATA"
0x0000 00000 (/home/wida/gocode/atomic-demo/main.go:21) MOVQ "".&a+8(SP), AX
0x0005 00005 (/home/wida/gocode/atomic-demo/main.go:21) INCL (AX)
原子操作在底层使用专用cup指令支持的,X86平台下LOCK
和XADD
,LOCK
会锁总线是个指令前缀和其他指令配合实现原子操作,XADDL AX, (CX)
指令将 交换AX和CX指针指向的值,同时AX的值和CX指针指向的值相加,结果保存在CX指针指向的值。
比较并交换
原子性的比较addr指针指向的值和old是不是一样,一样的话就会发生交换,返回值是布尔型,会返回是否交换。 有如下几个api函数:
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)
package main
import (
"fmt"
"sync/atomic"
)
func main(){
var a int32=100
b := atomic.CompareAndSwapInt32(&a,100,101)
fmt.Println(b,a)
b = atomic.CompareAndSwapInt32(&a,100,101)
fmt.Println(b,a)
}
$ go run main.go
true 101
同样我们看下汇编底层的代码
$ go build -gcflags -S 2>&1 | grep ".go:8" |grep -v "PCDATA"
0x0052 00082 (/home/wida/gocode/atomic-demo/main.go:8) MOVL $100, AX
0x0057 00087 (/home/wida/gocode/atomic-demo/main.go:8) MOVL $101, DX
0x005c 00092 (/home/wida/gocode/atomic-demo/main.go:8) LOCK
0x005d 00093 (/home/wida/gocode/atomic-demo/main.go:8) CMPXCHGL DX, (CX) #如果(CX)值和AX值相等,这(CX)值变成DX的值,同时修改 zf值为1,否则zf为0
0x0060 00096 (/home/wida/gocode/atomic-demo/main.go:8) SETEQ AL
0x0063 00099 (/home/wida/gocode/atomic-demo/main.go:8) MOVB AL, "".b+71(SP)
载入和存储
原子性加载数据,api有如下几个
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)
原子性存储数据,api有如下几个
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)
package main
import (
"fmt"
"sync/atomic"
"time"
)
func main(){
var a int32=100
if atomic.LoadInt32(&a) == 100 {
fmt.Println("that's 100")
}
b := atomic.LoadInt32(&a) + 10
fmt.Println(b)
for i:=0;i<100;i++ {
go func(i int) {
atomic.StoreInt32(&a,int32(i))
}(i)
}
time.Sleep(3e9)
fmt.Println(a)
}
$ go run main.go
that's 100
110
99
LoadInt32
和StoreInt32
汇编代码中可以看到都没有使用LOCK锁总线,因为到汇编底层这两个操作都是一个汇编指令,所以本身就具有原子性。
交换
原子性替换addr指针的值为new值,返回值返回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)
package main
import (
"fmt"
"sync/atomic"
)
func main(){
var a int32=100
old := atomic.SwapInt32(&a,101)
fmt.Println(old,a)
}
$ go run main.go
100 101
SwapInt32
和CompareAndSwapInt32
在汇编底层指令上不一样,SwapInt32
同样也是一条汇编指令,所以也没有用到LOCK
。
新类型Value
我们看到上面的几个api都是数值类型的,那么非数值类似的原子操作怎么使用呢?atomic在Go语言在1.4中加入了新的类型Value,带有Load和Store接口。
type Value struct {
v interface{}
}
func (v *Value) Load() (x interface{})
func (v *Value) Store(x interface{})
package main
import (
"fmt"
"sync/atomic"
"time"
)
func loadConfig() map[string]string {
return make(map[string]string)
}
func main() {
var config atomic.Value
config.Store(loadConfig())
go func() {
for {
time.Sleep(1 * time.Second)
config.Store(loadConfig())
}
}()
for i := 0; i < 10; i++ {
time.Sleep(1 * time.Second)
c := config.Load()
fmt.Printf("%p \n", c) //打印 c的指针
}
}
$ go run main.go
0xc00008c000
0xc00008c000
0xc000076180
0xc00008c030
0xc00008c030
0xc0000761b0
0xc0000761e0
0xc0000761e0
0xc000076210
0xc00009a060
总结
本小节介绍了golang的原子操作,原子操作的使用在并发系统里头十分的重要,比锁快,没死锁问题对并发编程十分的友好。