sync/atomic标准库包中提供的原子操作

    注意,本文中的很多例子并非并发程序。它们只是用来演示如何使用标准库包中提供的原子操作。

    对于一个整数类型Tsync/atomic标准库包提供了下列原子操作函数。其中T可以是内置int32int64uint32uint64uintptr类型。

    比如,下列五个原子操作函数提供给了内置int32类型。

    1. func AddInt32(addr *int32, delta int32)(new int32)
    2. func LoadInt32(addr *int32) (val int32)
    3. func StoreInt32(addr *int32, val int32)
    4. func SwapInt32(addr *int32, new int32) (old int32)
    5. func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)

    下列四个原子操作函数提供给了(安全)指针类型。因为Go目前(1.13)并不支持自定义范型,所以这些函数是通过unsafe.Pointer来实现的。

    1. func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
    2. func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
    3. func SwapPointer(addr *unsafe.Pointer, new T) (old unsafe.Pointer)
    4. func CompareAndSwapPointer(addr *unsafe.Pointer,
    5. old, new unsafe.Pointer) (swapped bool)

    因为Go指针不支持算术运算,所以相对于整数类型,指针类型的的原子操作少了一个AddPointer函数。 sync/atomic标准库包也提供了一个Value类型。以它为基的指针类型*Value拥有两个方法:LoadStoreValue值用来原子读取和修改任何类型的Go值。

    本文的余下部分将通过一些示例来展示如何使用这些原子操作函数。

    下面这个例子展示了如何使用add原子操作来并发地递增一个int32值。在此例子中,主协程中创建了1000个新协程。每个新协程将整数n的值增加1。原子操作保证这1000个新协程之间不会发生数据竞争。此程序肯定打印出1000

    1. package main
    2. import (
    3. "fmt"
    4. "sync"
    5. "sync/atomic"
    6. )
    7. func main() {
    8. var n int32
    9. var wg sync.WaitGroup
    10. for i := 0; i < 1000; i++ {
    11. wg.Add(1)
    12. go func() {
    13. atomic.AddInt32(&n, 1)
    14. wg.Done()
    15. }()
    16. wg.Wait()
    17. fmt.Println(atomic.LoadInt32(&n)) // 1000
    18. }

    如果我们将新协程中的语句atomic.AddInt32(&n, 1)替换为n++,则最后的输出结果很可能不是1000StoreTLoadT原子操作函数经常被用来需要并发运行的实现setter和getter方法。下面是一个这样的例子:

    1. views uint32
    2. }
    3. func (page *Page) SetViews(n uint32) {
    4. atomic.StoreUint32(&page.views, n)
    5. }
    6. func (page *Page) Views() uint32 {
    7. return atomic.LoadUint32(&page.views)
    8. }
    • 第二个实参为类型为T的一个变量值v。因为-v在Go中是合法的,所以-v可以直接被用做AddT调用的第二个实参。
    • 第二个实参为一个正整数常量c,这时-c在Go中是编译不通过的,所以它不能被用做AddT调用的第二个实参。这时我们可以使用^T(c-1)(仍为一个正数)做为AddT调用的第二个实参。

    ^T(v-1)小技巧对于无符号类型的变量v也是适用的,但是^T(v-1)T(-v)的效率要低。

    对于这个^T(c-1)小技巧,如果c是一个类型确定值并且它的类型确实就是T,则它的表示形式可以简化为^(c-1)。 一个例子:

    SwapT函数调用和StoreT函数调用类似,但是返回修改之前的旧值(因此称为置换操作)。

    一个CompareAndSwapT函数调用仅在新值和旧值不相等的情况下才会执行修改操作,并返回true;否则立即返回false。 一个例子:

    1. package main
    2. import (
    3. "fmt"
    4. "sync/atomic"
    5. )
    6. func main() {
    7. var n int64 = 123
    8. var old = atomic.SwapInt64(&n, 789)
    9. fmt.Println(n, old) // 789 123
    10. swapped := atomic.CompareAndSwapInt64(&n, 123, 456)
    11. fmt.Println(swapped) // false
    12. swapped = atomic.CompareAndSwapInt64(&n, 789, 456)
    13. fmt.Println(n) // 456
    14. }

    请注意,到目前为止(Go 1.13),一个64位字(int64或uint64值)的原子操作要求此64位字的内存地址必须是8字节对齐的。请阅读一文获取详情。

    上面已经提到了sync/atomic标准库包为指针值的原子操作提供了四个函数,并且指针值的原子操作是通过非类型安全指针来实现的。

    从一文,我们得知,在Go中,任何指针类型的值可以被显式转换为非类型安全指针类型unsafe.Pointer,反之亦然。所以指针类型*unsafe.Pointer的值也可以被显式转换为类型unsafe.Pointer,反之亦然。 下面这个程序不是一个并发程序。它仅仅展示了如何使用指针原子操作。在这个例子中,类型T可以为任何类型。

    1. package main
    2. import (
    3. "fmt"
    4. "sync/atomic"
    5. "unsafe"
    6. )
    7. type T struct {a, b, c int}
    8. var pT *T
    9. func main() {
    10. var unsafePPT = (*unsafe.Pointer)(unsafe.Pointer(&pT))
    11. var ta, tb T
    12. // 修改
    13. atomic.StorePointer(
    14. unsafePPT, unsafe.Pointer(&ta))
    15. // 读取
    16. pa1 := (*T)(atomic.LoadPointer(unsafePPT))
    17. fmt.Println(pa1 == &ta) // true
    18. // 置换
    19. pa2 := atomic.SwapPointer(
    20. unsafePPT, unsafe.Pointer(&tb))
    21. fmt.Println((*T)(pa2) == &ta) // true
    22. // compare and swap
    23. b := atomic.CompareAndSwapPointer(
    24. unsafePPT, pa2, unsafe.Pointer(&tb))
    25. fmt.Println(b) // false
    26. b = atomic.CompareAndSwapPointer(
    27. unsafePPT, unsafe.Pointer(&tb), pa2)
    28. }

    是的,目前指针的原子操作使用起来是相当的啰嗦。事实上,啰嗦还是次要的,更主要的是,因为指针的原子操作需要引入unsafe标准库包,所以这些操作函数不在Go 1兼容性保证之列。

    如果你确实担忧这些指针原子操作在未来的合法性,你可以使用下一节将要介绍的原子操作。但是下一节将要介绍的原子操作对于指针值来说比本节介绍的指针原子操作效率要低得多。

    sync/atomic标准库包中提供的Value类型可以用来读取和修改任何类型的值。

    类型*Value有两个方法:LoadStore。它没有AddSwap方法。

    Load方法的结果类型和Store方法的参数类型均为interface{}。所以Store方法调用的实参可以是任何类型的值。但是对于一个可寻址的Value类型的值v,一旦v.Store方法((&v).Store的简写形式)被曾经调用一次,则传递给此方法的后续调用的实参的具体类型必须和传递给它的第一次调用的实参的具体类型一致;否则,将产生一个恐慌。nil接口类型实参也将导致方法调用产生恐慌。 一个例子:

    事实上,我们也可以使用上一节介绍的指针原子操作来对任何类型的值进行原子读取和修改,不过需要多一级指针的间接引用。两种方法有各自的好处和缺点。在实践中需要根据具体需要选择合适的方法。

    为了便于理解和使用简单,Go值的原子操作被设计的和内存顺序保证无关。没有任何官方文档规定了原子操作应该保证的内存顺序。详见Go中的内存顺序保证一文对此情况的说明。

    Go语言101项目目前同时托管在和Gitlab上。欢迎各位在这两个项目中通过提交bug和PR的方式来改进完善Go语言101中的各篇文章。

    本书微信公众号名称为"Go 101"。每个工作日此公众号将尽量发表一篇和Go语言相关的原创短文。各位如果感兴趣,可以搜索关注一下。