Go 原子操作sync.atomic

 

sync.atomic

atomic 提供的原子操作能够确保任一时刻只有一个goroutine对几种简单的类型进行原子操作进行操作,善用atomic能够避免程序中出现大量的锁操作。
这些类型包括int32,int64,uint32,uint64,uintptr,unsafe.Pointer,共6个。
这些函数的原子操作共有5种:增或减,比较并交换、载入、存储和交换它们提供了不同的功能。

atomic常见操作有:

  • 增减
  • 载入
  • 比较并交换
  • 交换
  • 存储

 

增减

原子增或减即可实现对被操作值的增大或减少。因此该操作只能操作数值类型。
被用于进行增或减的原子操作都是以“Add”为前缀,并后面跟针对具体类型的名称。
atomic 包中提供了如下以Add为前缀的增减操作:
- 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 opts int64 = 0

   for i := 0; i < 50; i++ { 
       // 注意第一个参数必须是地址
       atomic.AddInt64(&opts, 3) //加操作
       //atomic.AddInt64(&opts, -1) 减操作
       time.Sleep(time.Millisecond)
   }
   time.Sleep(time.Second)

   fmt.Println("opts: ", atomic.LoadInt64(&opts))
}

由于atomic.AddUint32函数和atomic.AddUint64函数的第二个参数的类型分别是uint32和uint64,所以我们无法通过传递一个负的数值来减小被操作值。那么,这是不是就意味着我们无法原子的减小uint32或uint64类型的值了呢?幸好,不是这样。Go语言为我们提供了一个可以迂回的达到此目的办法。
如果我们想原子的把uint32类型的变量ui32的值增加NN(NN代表了一个负整数),那么我们可以这样调用atomic.AddUint32函数:

atomic.AddUint32(&ui32, ^uint32(-NN-1))

对于uint64类型的值来说也是这样。调用表达式

atomic.AddUint64(&ui64, ^uint64(-NN-1))

表示原子的把uint64类型的变量ui64的值增加NN(或者说减小-NN)。
之所以这种方式可以奏效,是因为它利用了二进制补码的特性。我们知道,一个负整数的补码可以通过对它按位(除了符号位之外)求反码并加一得到。我们还知道,一个负整数可以由对它的绝对值减一并求补码后得到的数值的二进制表示来代表。例如,如果NN是一个int类型的变量且其值为-35,那么表达式

uint32(int32(NN))

^uint32(-NN-1)

的结果值就都会是11111111111111111111111111011101。由此,我们使用^uint32(-NN-1)和^uint64(-NN-1)来分别表示uint32类型和uint64类型的NN就顺理成章了。这样,我们就可以合理的绕过uint32类型和uint64类型对值的限制了。
以上是官方提供一种通用解决方案。

载入

atomic 包中提供了如下以Load为前缀的增减操作:

- 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)

载入操作能够保证原子的读变量的值,当读取的时候,任何其他CPU操作都无法对该变量进行读写,其实现机制受到底层硬件的支持。
例子:

v := atomic.LoadInt32(&value)

函数atomic.LoadInt32接受一个*int32类型的指针值,并会返回该指针值指向的那个值。

比较并交换

并交换操作的英文称谓——Compare And Swap,简称CAS。在sync/atomic包中,这类原子操作由名称以“CompareAndSwap”为前缀的若干个函数代表。
该操作简称 CAS(Compare And Swap)。 这类操作的前缀为 CompareAndSwap :

- 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)

该操作在进行交换前首先确保变量的值未被更改,即仍然保持参数 old 所记录的值,满足此前提下才进行交换操作。CAS的做法类似操作数据库时常见的乐观锁机制。

需要注意的是,当有大量的goroutine 对变量进行读写操作时,可能导致CAS操作无法成功,这时可以利用for循环多次尝试。

例子:

CompareAndSwapInt64函数接受三个参数。第一个参数的值应该是指向被操作值的指针值。该值的类型即为*int64。后两个参数的类型都是int64类型。它们的值应该分别代表被操作值的旧值和新值。CompareAndSwapInt64函数在被调用之后会先判断参数addr指向的被操作值与参数old的值是否相等。仅当此判断得到肯定的结果之后,该函数才会用参数new代表的新值替换掉原先的旧值。否则,后面的替换操作就会被忽略。这正是“比较并交换”这个短语的由来。CompareAndSwapInt64函数的结果swapped被用来表示是否进行了值的替换操作。

var value int64

func atomicAddOp(tmp int64) {
for {
       oldValue := value
       if atomic.CompareAndSwapInt64(&value, oldValue, oldValue+tmp) {
           return
       }
   }
}

与锁相比,CAS操作有明显的不同。它总是假设被操作值未曾被改变(即与旧值相等),并一旦确认这个假设的真实性就立即进行值替换。而使用锁则是更加谨慎的做法。我们总是先假设会有并发的操作要修改被操作值,并使用锁将相关操作放入临界区中加以保护。我们可以说,使用锁的做法趋于悲观,而CAS操作的做法则更加乐观。

CAS操作的优势是,可以在不形成临界区和创建互斥量的情况下完成并发安全的值替换操作。这可以大大的减少同步对程序性能的损耗。当然,CAS操作也有劣势。在被操作值被频繁变更的情况下,CAS操作并不那么容易成功。

请记住,当想并发安全的更新一些类型(更具体的讲是,前文所述的那6个类型)的值的时候,我们总是应该优先选择CAS操作。

交换

此类操作的前缀为 Swap:

- 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)

相对于CAS,明显此类操作更为暴力直接,并不管变量的旧值是否被改变,直接赋予新值然后返回背替换的值。

 

存储

此类操作的前缀为 Store:

- 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)

此类操作确保了写变量的原子性,避免其他操作读到了修改变量过程中的脏数据。

 

 

笔记来源:

《GO并发编程实战》—— 原子操作

Go 原子操作

posted @ 2020-02-28 20:43  -零  阅读(1498)  评论(0编辑  收藏