go原子操作

go原子性用法

用法示例1:原子性增加值

package main

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

func main() {
  var count int32
  var wg sync.WaitGroup

  for i := 0; i < 10; i++ {
    wg.Add(1)
    go func() {
      atomic.AddInt32(&count, 1) // 原子性增加值
      wg.Done()
    }()
    go func() {
      fmt.Println(atomic.LoadInt32(&count)) // 原子性加载
    }()
  }
  wg.Wait()
  fmt.Println("count: ", count)
}

用法示例2:简易自旋锁实现

package main

import (
  "sync/atomic"
)

type spin int64

func (l *spin) lock() bool {
  for {
    if atomic.CompareAndSwapInt64((*int64)(l), 0, 1) {
      return true
    }
    continue
  }
}

func (l *spin) unlock() bool {
  for {
    if atomic.CompareAndSwapInt64((*int64)(l), 1, 0) {
      return true
    }
    continue
  }
}

func main() {
  s := new(spin)

  for i := 0; i < 5; i++ {
    s.lock()
    go func(i int) {
      println(i)
      s.unlock()
    }(i)
  }
  for {

  }
}

用法示例3: 无符号整数减法操作

atomicSubstractT
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddUint64(addr *uint64, delta uint64) (new uint64)

V-V
package main

import (
  "sync/atomic"
)

func main() {
  var i uint64 = 100
  var j uint64 = 10
  var k = 5
  atomic.AddUint64(&i, -j)
  println(i)
  atomic.AddUint64(&i, -uint64(k))
  println(i)
  // 下面这种操作是不可以的,会发生恐慌:constant -5 overflows uint64
  // atomic.AddUint64(&i, -uint64(5))
}

原子操作介绍

原子操作是指一次操作是不可被打断分割的(非原子操作,比如我们自己写的一个函数执行可能是会被在某个语句中断一会儿后接着继续执行的),这里说的原子操作专门指需要依赖CPU硬件指令提供的方式

go中的Cas操作,是借用了CPU提供的原子性指令来实现。CAS操作修改共享变量时候不需要对共享变量加锁,而是通过类似乐观锁的方式进行检查,本质还是不断的占用CPU 资源换取加锁带来的开销(比如上下文切换开销)。

CAS

CAS(Compare And Swap),这个其实是一个CPU指令其作用是让CPU比较内存中某个值是否和预期的值相同,如果相同则将这个值更新为新值,不相同则不做更新,由CPU保证这个过程的原子性。这是一个非常底层的函数,用在并发场景中非常有用。一般用来在并发场景中尝试修改值,也是自旋锁的底层。

CAS的缺点

CAS

ABA问题

CASABA
  1. 线程1,期望值为A,欲更新的值为B
  2. 线程2,期望值为A,欲更新的值为B
12132222A->B->A
ABA
+1A->B->A1A->2B->3A

循环时间长开销大

CASCAS

这种循环也称为自旋

解决方法: 限制自旋次数,防止进入死循环。

只能保证一个共享变量的原子操作

CAS
CAS

互斥锁

互斥锁:一个变量n两个线程持续不断的将n的值增加1,为避免并发问题,用一把锁来保护变量n,锁的本质也是一个变量,初始值置为0,若能够原子性的将锁的值由0置为1,就可以获得锁,然后操作变量n,操作结束后原子性将锁的值由1置为0,就可以释放锁,但现在有两个线程都会尝试获得锁,而同一时刻只有一个能够成功,另一个没获取锁的循环获取锁,直到能获取锁而已。

另一个线程循环获取锁会增加CPU负担,增加pause来解决,循环30次获取不到就不获取了,程序运行的时间会明显提高。

本质上也是一个变量
只允许一个访问者对其进行访问唯一性排它性

一般来讲我们说锁都是指操作系统级别通过互斥来进行调度的方式,自旋锁是特指依赖CAS进行资源抢占的方式(也有的地方把CAS自旋这种叫做无锁设计,概念比较混乱)。而Java语言中直接使用互斥锁比较重,在某些场景下可以在JVM层面做一些轻量级的调度,所以它创造了很多概念。所以重量级锁就是synchronized关键字,底层是互斥锁。偏向锁、轻量级锁、自旋锁底层都是CAS。

原子操作与互斥锁的区别

首先atomic操作的优势是更轻量,比如CAS可以在不形成临界区和创建互斥量的情况下完成并发安全的值替换操作。这可以大大的减少同步对程序性能的损耗。

原子操作也有劣势。还是以CAS操作为例,使用CAS操作的做法趋于乐观,总是假设被操作值未曾被改变(即与旧值相等),并一旦确认这个假设的真实性就立即进行值替换,那么在被操作值被频繁变更的情况下,CAS操作并不那么容易成功。而使用互斥锁的做法则趋于悲观,我们总假设会有并发的操作要修改被操作的值,并使用锁将相关操作放入临界区中加以保护。

下面是几点区别:

  • 互斥锁是一种数据结构,用来让一个线程执行程序的关键部分,完成互斥的多个操作
  • 可以把互斥锁理解为悲观锁,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程
  • 原子操作是无锁的,常常直接通过CPU指令直接实现
  • 原子操作中的cas趋于乐观锁,CAS操作并不那么容易成功,需要判断,然后尝试处理
atomic

内存屏障

在编译器层面也会对我们写的代码做优化导致CPU看到的指令顺序跟我们写的代码顺序并不完全是一致的,这就也会导致多核执行情况下,数据不一致问题。而内存屏障也是解决这些问题的一种手段,各个语言封装底层指令,强制CPU指令按照代码写的顺序执行

在上文中可以看到为提供缓冲命中和减少与内存通信频率,CPU做了各种优化策略,有的会给我们带来一些问题,比如某个核心更新了数据之后,如果没有进行原子操作会导致各个核心在L1中的数据不一致问题。

内存屏障另一个作用是强制更新CPU的缓存,比如一个写屏障指令会把这个屏障前写入的数据更新到缓存中,这样任何后面试图读取该数据的线程都将得到最新值。

程序执行

代码编写,编译器编译成可执行文件,作为进程运行在物理内存,物理内存映射到虚拟内存(连续完整的地址空间),cpu运算,操作系统线程调度、程序结束等。

参考