Go 同步与锁

125 阅读4分钟

这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战

基本原语

go提供的在并发计算中最基本的同步原语(相比channel更为原始),主要有

  • sync.Mutex
  • sync.RWMutex
  • sync.WaitGroup
  • sync.Once
  • sync.Cond

Mutex

互斥锁

type Mutex struct {
	state int32
	sema  uint32 // 等待队列
}

state不同位有不同的含义,表示当前互斥锁的状态,从高到低分别是

  • mutexLocked:互斥锁的锁定状态
  • mutexWoken:从正常模式被从唤醒
  • mutexStarving:互斥锁进入饥饿模式
  • waitersCount:当前互斥锁上面在等待的Goroutine数量

正常模式与饥饿模式

  • 正常模式:等待者按照先进先出的顺序获得锁
    • 性能更好
    • 先进先出指的是Goroutine抢锁的顺序,而不是等待的顺序,旧的goroutine会被唤醒与新的goroutine竞争
      • 没睡醒的肯定争不过刚来的
  • 饥饿模式:goroutine超过1ms没有获取到锁,进入饥饿模式,防止被饿死
    • 变成队列的形式排队进行锁的获取

加锁和解锁

加锁:

  • 使用CAS的方式(compare and swap)
  • 使用自旋锁来检查当前锁是否可用
    • 自旋锁:goroutine反复检查锁变量是否可用,直到获取该锁,持有该锁直到完成操作
    • 自旋锁会持续占用CPU,避免Goroutine的切换,在多核下才有意义,否则自旋等锁没有人能够释放锁
    • 条件
      • 正常模式
      • 多核
      • 自旋次数小于4
      • 存在一个正在运行的处理器P,且等待队列为空
    • 每次旋都会检查当前锁的状态
    • 尝试通过CAS更新锁
      • 如果是正常模式,直接退出即可
      • 如果是在队列,出队
        • 只有当前Goroutine,从饥饿模式退出

解锁:

  • 直接减去加锁偏移量解锁就好了
  • 解锁不成功的时候进入sync.Mutex.unlockSlow
    • 正常模式解完直接返回
    • 饥饿模式从队列唤醒等待者

RWMutex

mutex只要需要访问该资源都需要上锁,RWMutex不限制并发读,只是不允许并发写与读写,有着更高的性能

type RWMutex struct {
	w           Mutex // 复用锁能力
	writerSem   uint32 // 写等待
	readerSem   uint32 // 读等待
	readerCount int32 // 当前并发读数量
	readerWait  int32 // 写阻塞是
}

主要有四个函数,分别针对读、写的上锁与解锁操作

写锁

sync.RWMutex.Lock()上锁

  • 使用w上锁阻塞后续其他写操作
  • 获取当前读队列,并且将读队列置为负数
    • 不为零,并且无法阻塞其他读操作时
    • 当前写goroutine进入休眠,等待唤醒

sync.RWMutex.Unlock()解锁

  • 将readcounter恢复为pending的reader数量,释放读锁,释放读routine
  • 释放写锁

先读后写,保证读goroutine不会被饿死

读锁

sync.RWMutex.RLock()上锁

  • 如果readerCounter为负数,说明当前有写Goroutine,当前读Goroutine进入休眠

sync.RWMutex.RUnlock()解锁

  • 减少读队列
    • 如果读队列小于0,说明有写操作在进行
    • 减少等待队列
    • 尝试解锁写goroutine

WaiteGroup

等待一组Goroutine返回,常用于多Groutine的同步,盲猜使用了信号量

type WaitGroup struct {
	noCopy noCopy
	state1 [3]uint32 // 存储当前结构体的状态
}
  • noCopy用于防止被拷贝(值复制),防止同步数量对不上
    • 在编译的时候会进行检查,如果用户进行了值拷贝,会报错

三个方法

Add():

  • 更新内部的计数器
  • 传入的可以是一个负值
  • 计数器为负值的时候会报错
  • 当计数器为0的时候会唤醒所有等待的Goroutine

Waite():

  • 如果计数器为0,直接过
  • 否则休眠Goroutine计数器+1,并且使得当前Goroutine休眠

Done():

  • 实际上就是调用了Add(),传入-1

Once

能够保证内部的函数只执行了1次

type Once struct {
	done uint32
	m    Mutex
}

只唯一向外暴露了一个方法

Do()

接受参数为一个无参函数

  • 判断一下是否执行过
  • 没有执行过
    • 上锁
    • 执行无参函数
    • 标记为执行过

Cond

用于多个Goroutine等待一个条件再启动,条件广播

type Cond struct {
	noCopy  noCopy
	L       Locker // 保护内部notifyList字段
	notify  notifyList // goroutine链表
	checker copyChecker
}
type notifyList struct {
	wait uint32 // 正在等待的Goroutine索引
	notify uint32 // 已经被唤醒的Goroutine索引

	lock mutex
	head *sudog
	tail *sudog
}

Wait()

使当前Goroutine进入休眠

  • 加入notify list
  • 加入wait list的末端,并且使当前Goroutine陷入休眠

Signal()

使队列最前面的Goroutine被唤醒

Broadcast()

唤醒队列中所有的Goroutine