kevinyan815 / gocookbook

go cook book
MIT License
768 stars 162 forks source link

原子操作用法详解 #65

Open kevinyan815 opened 2 years ago

kevinyan815 commented 2 years ago

啥是原子操作呢?顾名思义,原子操作就是具备原子性的操作... 是不是感觉说了跟没说一样,原子性的解释如下:

一个或者多个操作在 CPU 执行的过程中不被中断的特性,称为原子性(atomicity) 。这些操作对外表现成一个不可分割的整体,他们要么都执行,要么都不执行,外界不会看到他们只执行到一半的状态。

Go 语言提供了哪些原子操作

Go语言通过内置包sync/atomic提供了对原子操作的支持,其提供的原子操作有以下几大类:

互斥锁跟原子操作的区别

原子操作增加、载入

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)
        }()
    }
    wg.Wait()
    timeSpends := time.Now().Sub(start).Nanoseconds()
    fmt.Printf("use atomic a is %d, spend time: %v\n", atomic.LoadInt32(&a), timeSpends)
}

完整源码,请访问atomic示例一

需要注意的是,所有原子操作方法的被操作数形参必须是指针类型,通过指针变量可以获取被操作数在内存中的地址,从而施加特殊的CPU指令,确保同一时间只有一个goroutine能够进行操作

比较并交换

该操作简称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的做法类似操作数据库时常见的乐观锁机制。

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类型对外暴露了两个方法:

1.17 版本我看还增加了SwapCompareAndSwap方法。

下面是一个简单的例子演示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示例二

你也可以试试,不用atomic.Value,直接给Rectange类型的指针变量赋值,看看在并发条件下,两个字段的值是不是能跟预期的一样变成10和15。

总结

原子操作由底层硬件支持,而锁则由操作系统的调度器实现。锁应当用来保护一段逻辑,对于一个变量更新的保护,原子操作通常会更有效率,并且更能利用计算机多核的优势,如果要更新的是一个复合对象,则应当使用atomic.Value封装好的实现。