GO的原子操作

GO的原子操作

一、原子操作

互斥锁虽然可以保证临界区中代码的串行执行,但却不能保证这些代码执行的原子性。

在同一时刻,只可能有少数的 goroutine 真正地处于运行状态,并且这个数量只会与 M 的数量一致,而不会随着 G 的增多而增长。

为了公平起见,调度器总是会频繁地换上或换下这些 goroutine。

换上:让一个 goroutine 由非运行状态转为运行状态,并促使其中的代码在某个 CPU 核心上执行。

换下:使一个 goroutine 中的代码中断执行,并让它由运行状态转为非运行状态。

操作系统层面只对针对二进制位或整数的原子操作提供了支持。Go 语言的原子操作当然是基于 CPU 和操作系统的,所以它也只针对少数数据类型的值提供了原子操作函数。这些函数都存在于标准库代码包sync/atomic中。

二、sync/atomic提供了的原子操作种类,以及可操作的数据类型

sync/atomic包中的函数可以做的原子操作有:加法(add)、比较并交换(compare and swap,简称 CAS)、加载(load)、存储(store)和交换(swap)。

这些数据类型有:int32、int64、uint32、uint64、uintptr,以及unsafe包中的Pointer。不过,针对unsafe.Pointer类型,该包并未提供进行原子加法操作的函数。

对这些类型中的每一个,sync/atomic包都会有一套函数给予支持。

此外,sync/atomic包还提供了一个名为Value的类型,它可以被用来存储任意类型的值。

三、原子操作函数需要的是被操作值的指针,而不是这个值本身

比如,atomic.AddInt32函数的第一个参数,对应的一定是那个要被增大的整数。可是,这个参数的类型不是int32而是*int32

传入值本身没有任何意义。被传入函数的参数值都会被复制,像这种基本类型的值一旦被传入函数,就已经与函数外的那个值毫无关系了。

unsafe.Pointer类型虽然是指针类型,但是那些原子操作函数要操作的是这个指针值,而不是它指向的那个值,所以需要的仍然是指向这个指针值的指针。

只要原子操作函数拿到了被操作值的指针,就可以定位到存储该值的内存地址。只有这样,它们才能够通过底层的指令,准确地操作这个内存地址上的数据。

四、用于原子加法操作的函数可以做原子减法吗

当然是可以的。atomic.AddInt32函数的第二个参数代表差量,它的类型是int32,是有符号的。如果我们想做原子减法,那么把这个差量设置为负整数就可以了。

五、比较并交换操作与交换操作相比

比较并交换操作即 CAS 操作,是有条件的交换操作,只有在条件满足的情况下才会进行值的交换。

所谓的交换指的是,把新值赋给变量,并返回变量的旧值。

CAS 操作并不是单一的操作,而是一种操作组合。我们将它与for语句联用就可以实现一种简易的自旋锁(spinlock)。

for {
    
    
 if atomic.CompareAndSwapInt32(&num2, 10, 0) {
    
    
  fmt.Println("The second number has gone to zero.")
  break
 }
 time.Sleep(time.Millisecond * 500)
}

在for语句中的 CAS 操作可以不停地检查某个需要满足的条件,一旦条件满足就退出for循环。这就相当于,只要条件未被满足,当前的流程就会被一直“阻塞”在这里。

而for语句加 CAS 操作的假设往往是:共享资源状态的改变并不频繁,或者,它的状态总会变成期望的那样。这是一种更加乐观,或者说更加宽松的做法。

六、保证了对一个变量的写操作都是原子操作,读操作的时候,还有必要使用原子操作吗

很有必要。如果写操作还没有进行完,读操作就来读了,那么就只能读到仅修改了一部分的值。这显然破坏了值的完整性,读出来的值也是完全错误的。

七、怎样用好sync/atomic.Value

atomic.Value类型是开箱即用的,我们声明一个该类型的变量之后就可以直接使用了。这个类型使用起来很简单,它只有两个指针方法:Store和Load。

  • 一旦atomic.Value类型的值被真正使用,它就不应该再被复制了。

    只要用它来存储值了,就相当于开始真正使用了。

    atomic.Value类型属于结构体类型,而结构体类型属于值类型。所以,复制该类型的值会产生一个完全分离的新值。

还有两条强制性的使用规则:

  • 第一条规则,不能用原子值存储nil。

    不能把nil作为参数值传入原子值的Store方法,否则就会引发一个 panic。

  • 第二条规则,向atomic.Value类型的值中存储的第一个值,决定了它今后能且只能存储哪一个类型的值。

    先存储一个接口类型的值,然后再存储这个接口的某个实现类型的值,也是不可以的,同样会引发一个 panic。

几条具体的使用建议:

  1. 不要把内部使用的原子值暴露给外界。
  2. 如果可能的话,我们可以把原子值封装到一个数据类型中,比如一个结构体类型。
  3. 尽量不要向原子值中存储引用类型的值。因为这很容易造成安全漏洞。

猜你喜欢

转载自blog.csdn.net/hefrankeleyn/article/details/128603514