go语言标准库sync/atomic中的原子操作

原子操作吧其他同步技术更底层。他们没有锁,基本是在硬件层面实现的。事实上,他们经常被用来实现其他同步技术。

请注意,下面的许多例子并发并发编程。他们仅用于来展示如何使用标准库中的sync/atomic包中的原子函数。

go语言中的原子操作概览

标准库中的sync/atomic对整数类型T(包含int32,int64,uint32,uint64,uintptr)提供5种类型的原子函数。

func AddT(addr *T, delta T)(new T)
func LoadT(addr *T) (val T)
func StoreT(addr *T, val T)
func SwapT(addr *T, new T) (old T)
func CompareAndSwapT(addr *T, old, new T) (swapped bool)

下面是in32的

func AddInt32(addr *int32, delta int32)(new int32)
func LoadInt32(addr *int32) (val int32)
func StoreInt32(addr *int32, val int32)
func SwapInt32(addr *int32, new int32) (old int32)
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)

为(安全)指针类型提供以下四个原子函数:

func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
func SwapPointer(addr *unsafe.Pointer, new T) (old unsafe.Pointer)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)

上面并没有AddPointer函数,因为go的指针不支持算术运算

sync/atomic还支持一个类型Value. 它关联到指针类型*Value,有两个函数LoadStore,一个Value可以被用来原子的加载或者存储任何类型 的值

func (v *Value) Load() (x interface{})
func (v *Value) Store(x interface{})

本文的其余部分显示了有关如何使用Go中提供的原子操作的一些示例。

整数的原子操作

以下示例说明如何使用AddInt32函数对int32值执行add atomic操作.在此示例中,主goroutine创建了1000个新的并发goroutine。每个新创建的goroutine将整数n增加1。原子操作保证了这些goroutine之间没有数据竞争。最后,保证打印1000。

package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {
    var n int32
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            atomic.AddInt32(&n, 1)
            wg.Done()
        }()
    }
    wg.Wait()

    fmt.Println(atomic.LoadInt32(&n)) // 1000
}

如果需要同时使用类型的值,则StoreT和LoadT原子函数通常用于实现类型(相应指针类型)的setter和getter方法。例如,

type Page struct {
    views uint32
}

func (page *Page) SetViews(n uint32) {
    atomic.StoreUint32(&page.views, n)
}

func (page *Page) Views() uint32 {
    return atomic.LoadUint32(&page.views)
}

对于有符号整数类型T(int32或int64),调用AddT函数的第二个参数可以是负值,以执行原子减操作。但是如何对无符号累着执行原子减操作呢。对第二个无符号整型参数有两个中情况。

  1. 对无符号变量v, -v是合法的。所以我们把-v传给AddT的第二个参数。
  2. 对应正常量整数c,-c作为AddT的第二个参数是非法的,我们可以使用^T(c-1)作为AddT的第二个参数

    例子如下

package main

import (
    "fmt"
    "sync/atomic"
)

func main() {
    var (
        n uint64 = 97
        m uint64 = 1
        k int    = 2
    )
    const (
        a        = 3
        b uint64 = 4
        c uint32 = 5
        d int    = 6
    )

    atomic.AddUint64(&n, -m);             fmt.Println(n) // 96 (97 - 1)
    atomic.AddUint64(&n, -uint64(k));     fmt.Println(n) // 94 (95 - 2)
    atomic.AddUint64(&n, ^uint64(a - 1)); fmt.Println(n) // 91 (94 - 3)
    atomic.AddUint64(&n, ^(b - 1));       fmt.Println(n) // 87 (91 - 4)
    atomic.AddUint64(&n, ^uint64(c - 1)); fmt.Println(n) // 82 (87 - 5)
    atomic.AddUint64(&n, ^uint64(d - 1)); fmt.Println(n) // 76 (82 - 6)
    x := b; atomic.AddUint64(&n, -x);     fmt.Println(n) // 72 (76 - 4)
    atomic.AddUint64(&n, ^(m - 1));       fmt.Println(n) // 71 (72 - 1)
    atomic.AddUint64(&n, ^uint64(k - 1)); fmt.Println(n) // 69 (71 - 2)
}

一个SwapT函数调用与StoreT调用想像,但是返回原来的值。

CompareAndSwapT函数调用仅仅适用于当前值等于传入的旧值的时候存储新值。返回的bool值用来表示存储操作是否被执行。

package main

import (
    "fmt"
    "sync/atomic"
)

func main() {
    var n int64 = 123
    var old = atomic.SwapInt64(&n, 789)
    fmt.Println(n, old) // 789 123
    fmt.Println(atomic.CompareAndSwapInt64(&n, 123, 456)) // false
    fmt.Println(n) // 789
    fmt.Println(atomic.CompareAndSwapInt64(&n, 789, 456)) // true
    fmt.Println(n) // 456
}

请注意,到目前为止(Go 1.12),64位字,a.k.a。,int64和uint64值的原子操作要求64位字必须在内存中对齐8字节.

指针的原子操作

前文我们有提到对于指针的原子操作,标准库中提供的四个函数。

在go中,任何指针类型可以被转换为unsafe.Pointer,反之亦然。所以,*unsafe.Pointer类型也能被明确的转换成unsafe.Pointer类型,反之亦然。

下面的例子展示如何使用原子操作操作指针

package main

import (
    "fmt"
    "sync/atomic"
    "unsafe"
)

type T struct {a, b, c int}
var pT *T

func main() {
    var unsafePPT = (*unsafe.Pointer)(unsafe.Pointer(&pT))
    var ta, tb T
    // store
    atomic.StorePointer(unsafePPT, unsafe.Pointer(&ta))
    // load
    pa1 := (*T)(atomic.LoadPointer(unsafePPT))
    fmt.Println(pa1 == &ta) // true
    // swap
    pa2 := atomic.SwapPointer(unsafePPT, unsafe.Pointer(&tb))
    fmt.Println((*T)(pa2) == &ta) // true
    // compare and swap
    b := atomic.CompareAndSwapPointer(unsafePPT, pa2, unsafe.Pointer(&tb))
    fmt.Println(b) // false
    b = atomic.CompareAndSwapPointer(unsafePPT, unsafe.Pointer(&tb), pa2)
    fmt.Println(b) // true
}

是的,使用指针原子函数非常冗长。事实上,不仅用途冗长,它们也不受Go 1兼容性指南的保护,因为这些用途需要导入不安全的标准包。

任意类型值的原子操作

同步/原子标准包中提供的值类型可用于原子加载和存储任何类型的值。Type * Value有两种方法,Load和Store。添加和交换方法不适用于* Value类型.

Load和Store的参数都是interface{}。所以调用时Store可以使用任何类型作为参数,但是对于可寻址的Value 值v,一旦v.Store被调用过,接下来的v.Store必须采用同样类型的参数作为参数,否则将会panic。用nil作为参数也会导致panic。

例子:

package main

import (
    "fmt"
    "sync/atomic"
)

func main() {
    type T struct {a, b, c int}
    var ta = T{1, 2, 3}
    var v atomic.Value
    v.Store(ta)
    var tb = v.Load().(T)
    fmt.Println(tb)       // {1 2 3}
    fmt.Println(ta == tb) // true

    v.Store("hello") // will panic
}