原子操作

什么是原子操作

原子操作(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平台下LOCKXADD,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

LoadInt32StoreInt32 汇编代码中可以看到都没有使用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

SwapInt32CompareAndSwapInt32 在汇编底层指令上不一样,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的原子操作,原子操作的使用在并发系统里头十分的重要,比锁快,没死锁问题对并发编程十分的友好。