简介
sync/atomic 包实现了同步算法底层的原子的内存操作原语,我们把它叫做原子操作原语,它提供了一些实现原子操作的方法。
案例
看一下这个案例,我启动了 10 个 goruntine 来进行 10000 次 a++ 操作,不要看 a++ 就只有 1 条语句就不存在并发问题,其实底层汇编指令是有多条,存在并发问题。这里输出不会是 100000 。产生并发的根本原因是 a++ 不是原子操作。那么怎么解决呢?我们可以加锁,也可以使用我们今天说的 atomic 原子操作。
package main
import (
"fmt"
"sync"
)
func main() {
var a int32
var wg sync.WaitGroup
for i := 1; i <= 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 1; j <= 10000; j++ {
a++
}
}()
}
wg.Wait()
fmt.Println(a)
}
只要将上面 a++ 的代码修改成下面的代码,就能实现原子的 a++
atomic.AddInt32(&a, 1)
atomic 提供的方法
AddXXX操作,将传入地址的值加上 delta
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)
CompareAndSwapXXX操作,会先比较传入的地址的值是否是 old,如果是的话就尝试赋新值,如果不是的话就直接返回 false,返回 true 时表示赋值成功
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)
LoadXXX操作,从某个地址中取值
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)
StoreXXX操作,给地址进行赋值
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)
SwapXXX操作,交换两个值,并且返回老的值
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)
Load/Store,atomic 包下有一个 Value 结构体,用于任意类型的值的 Store、Load
type Value
func (v *Value) Load() (x interface{})
func (v *Value) Store(x interface{})
源码分析
除了 atomic.Value 是有源码的,其他类型需要去 runtime/internal/atomic 中找,看一下CAS 吧,从注释上也可以看出来是 CAS 操作,此外,这里还使用了 LOCK 来保证操作的原子性,这里了解下 LOCK指令 是 CPU 级别的锁就可以了。感兴趣的话可以私下去搜一下 LOCK指令。
// bool Cas(int32 *val, int32 old, int32 new)
// Atomically:
// if(*val == old){
// *val = new;
// return 1;
// }else
// return 0;
TEXT runtime∕internal∕atomic·Cas(SB), NOSPLIT, $0-13
MOVL ptr+0(FP), BX
MOVL old+4(FP), AX
MOVL new+8(FP), CX
LOCK
CMPXCHGL CX, 0(BX)
SETEQ ret+12(FP)
RET
atomic/Value
Value 这个结构比较简单,内部就一个 interface{} 的 v
type Value struct {
v interface{}
}
type ifaceWords struct {
typ unsafe.Pointer
data unsafe.Pointer
}
- Store 方法
使用 Store 保存值后,数据类型就固定下来了,后续操作时必须使用相同的数据类型,否则会 panic,且不能保存 nil。
如果是首次 Store 则会调用 runtime_procPin() 禁止其他 goruntine 抢占,然后使用 CAS 操作 ,将 typ 修改为中间值 unsafe.Pointer(^uintptr(0)),然后设置真正的 typ 和 data
如果看到 if uintptr(typ) == ^uintptr(0) 这行代码,如果为 true 则表示第一次赋值操作还没有完成,需要进行等待,如果是 false 就直接原子的设置 data 就可以了。
// 存储 x
// 调用者需要传入相同的 x 类型
// 如果传入不同的 x 类型或者 nil ,就会 Panic
func (v *Value) Store(x interface{}) {
// x 为 nil 就 panic
if x == nil {
panic("sync/atomic: store of nil value into Value")
}
vp := (*ifaceWords)(unsafe.Pointer(v)) // 获取当前值
xp := (*ifaceWords)(unsafe.Pointer(&x)) // 获取设置值
for {
typ := LoadPointer(&vp.typ) // 原子的获取当前值的类型
if typ == nil { // 如果类型为 nil ,就代表之前没有值,然后需要进行赋值逻辑
// 调用 runtime 的方法禁止其他 goruntine 抢占,避免操作完成一半就被抢占了
runtime_procPin()
// 然后使用 CAS 操作将类型设置成 ^uintptr(0) 这个中间状态
if !CompareAndSwapPointer(&vp.typ, nil, unsafe.Pointer(^uintptr(0))) {
runtime_procUnpin()
continue
}
// 原子设置真正的 type 和 data
StorePointer(&vp.data, xp.data)
StorePointer(&vp.typ, xp.typ)
// 恢复可抢占状态
runtime_procUnpin()
// 退出完成
return
}
// 如果等于 ^uintptr(0) 中间状态,就代表第一次赋值还没有完成,需要等待
if uintptr(typ) == ^uintptr(0) {
continue
}
// 类型不相同 panic
if typ != xp.typ {
panic("sync/atomic: store of inconsistently typed value into Value")
}
// 设置data
StorePointer(&vp.data, xp.data)
return
}
}
- Load 方法
Load 方法比较简单,就是一些原子的读取
func (v *Value) Load() (x interface{}) {
vp := (*ifaceWords)(unsafe.Pointer(v))
typ := LoadPointer(&vp.typ) // 原子读 typ
// 没有类型或者指针第一次赋值没有成功,都返回 nil
if typ == nil || uintptr(typ) == ^uintptr(0) {
return nil
}
data := LoadPointer(&vp.data) // 原子的读取data
// 构造 x 返回
xp := (*ifaceWords)(unsafe.Pointer(&x))
xp.typ = typ
xp.data = data
return
}
应用场景举例
假设有这样的一个场景,配置可能是从第三方服务中动态获取的,比如是 Watch Etcd 中的一个 key ,这种一般会单独启动一个 goruntine 来做配置动态变更。然后其他 goruntine 在使用这个配置的时候就需要做原子的读取,就可以使用今天说的 atomic.Value来实现。下面的案例配置加载的例子,最简陋版本。
package main
import (
"fmt"
"math/rand"
"sync"
"sync/atomic"
"time"
)
type Config struct {
version int32
}
func main() {
var config atomic.Value
go func() {
for {
// 原子的写入
config.Store(&Config{version: rand.Int31n(1000)})
// sleep 来模拟从etcd 获取
time.Sleep(time.Millisecond * 1)
}
}()
var wg sync.WaitGroup
for i := 1; i <= 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 1; j <= 100; j++ {
// 使用原子的获取
c, ok := config.Load().(*Config)
if !ok {
continue
}
fmt.Println(c.version)
}
}()
}
wg.Wait()
}
一个引发思考的案子
了解过 Go 内存模型 (不知道的可以看一下我的这篇文章 Go内存模型 )会知道机器字,Go操作一个机器字是原子性的,但是像 interface 结构的就不是一个机器字。
看一下下面这个例子,这个是 Dave 写的 IceCreamMaker 问题,先想一下输出会上啥样子的。
package main
import "fmt"
type IceCreamMaker interface {
// Hello greets a customer
Hello()
}
type Ben struct {
name string
}
func (b *Ben) Hello() {
fmt.Printf("Ben says, "Hello my name is %s"\n", b.name)
}
type Jerry struct {
name string
}
func (j *Jerry) Hello() {
fmt.Printf("Jerry says, "Hello my name is %s"\n", j.name)
}
func main() {
var ben = &Ben{"Ben"}
var jerry = &Jerry{"Jerry"}
var maker IceCreamMaker = ben
var loop0, loop1 func()
loop0 = func() {
maker = ben
go loop1()
}
loop1 = func() {
maker = jerry
go loop0()
}
go loop0()
for {
maker.Hello()
}
}
可能的输出结果:
Ben says, "Hello my name is Ben"
Jerry says, "Hello my name is Jerry"
Jerry says, "Hello my name is Jerry"
Ben says, "Hello my name is Jerry"
Ben says, "Hello my name is Ben"
存在Ben says, "Hello my name is Jerry"输出的原因是因为 interface 的实现有关系,interface 底层是有 2 个机器字,分别是 Type 和 Data 2个指针组成的,但是我们的循环一直在打印 Hello 方法,可能 Type 正好改过来了,Data 没有改过来,然后就出现了这种输出。
type interface struct {
Type uintptr // 指向接口实现的类型
Data uintptr // 保存接口接收者的数据
}
由于 interface 的赋值不是原子的,所以我们观察到了一个中间状态,这种对于程序来说是不能容忍的,所以一定要小心,这点我之前也没有关注过。
LockFree
基于 atomic 可以实现无锁队列,具体可以参考一下鸟窝大佬的文章,我在参考中列了。