go 进阶 sync相关: 四. sync.Cond 同步等待条件

一. 基础

  1. Cond相关方法
  1. func NewCond(l Locker) *Cond: 创建一个Cond实例, 需要关联一个锁
  2. func (c *Cond) Wait(): 阻塞
  3. func (c *Cond) Broadcast(): 广播唤醒所有等待
  4. func (c *Cond) Signal(): 只唤醒任意1个等待条件变量
  1. 使用示例
func TestCond() {
	//1.创建一个Cond实例
	cond := sync.NewCond(&sync.Mutex{})
	for i := 0; i < 10; i++ {
		//协程
		go func(t int) {
			time.Sleep(time.Second)
			//2.加锁
			cond.L.Lock()
			//4.通过defer释放锁
			defer cond.L.Unlock()
			//3.阻塞
			cond.Wait()
			fmt.Println(t)
		}(i)
	}

	time.Sleep(2 * time.Second)
	//唤醒cond阻塞继续执行
	//cond.Signal()
	cond.Broadcast()
}

使用场景

  1. cond的主要作用就是获取锁之后,调用wait()方法阻塞等待通知,来进行下一步锁释放等操作,以此控制锁的释放时机,释放频率,适用于在并发环境下goroutine的等待和通知
  2. 例如有一个协程正在接收数据,其他协程必须等待这个协程接收完数据,才能读取到正确的数据
  3. 每个 Cond 都会关联一个 Lock ,当修改条件或者调用Wait方法,必须加锁保护 Condition, 有点类似Java中的Wait和NotifyAll
  4. 参考博客

二. 源码分析

  1. 常简单,关键的逻辑调用了运行时中的信号量代码,本文只分析与Cond相关的代码,详细信号量代码源码分析准备专门写一篇文章
  2. 我们先看一下sync\cond.go下的Cond这个结构体
type Cond struct {
	//noCopy可以嵌入到结构中,在第一次使用后不可复制,使用go vet作为检测使用
	noCopy noCopy
	//根据需求初始化不同的锁,如*Mutex 和 *RWMutex
	L Locker
	//通知列表,调用Wait()方法的goroutine会被放入list中,每次唤醒,从这里取出
	notify  notifyList
	//复制检查,检查cond实例是否被复制
	checker copyChecker
}
  1. 在Cond内部有一个notifyList 结构体,wait和notify都是一个计数器,它们的初始值都为0,每次调用Wait操作,wait的值都会增加1.wait的值可以理解为调用Wait操作程序所在的goroutine的编号,notify值表示小于它的阻塞的goroutine已经唤醒处理过,调用Signal或者Broadcast时唤醒阻塞在[notify,wait)范围编号上的goroutine。head和tail是一个单链表的头尾指针节点
type notifyList struct {
	//wait 表示当前 Wait 的最大 ticket 值
	wait   uint32
	//notify 表示目前已唤醒的 goroutine 的 ticket 的最大值
	notify uint32
	lock   uintptr
	//head 和 tail: 等待在这个 sync.Cond 上的 goroutine 链表
	head   unsafe.Pointer
	tail   unsafe.Pointer
}
  1. 通俗理解: notifyList为一个队列,它里面存储是goroutine。wait和notify分别表示生产者和消费者的位置。这个队列是一个单链表,里面的goroutine按照wait值从小到大排列

1. NewCond()创建Cond实例

  1. 调用NewCond()函数,传递一个sync.Mutex指针,创建Cond实例
func NewCond(l Locker) *Cond {
	return &Cond{L: l}
}

2. Wait()阻塞

  1. 查看Wait()函数,该函数内先后执行了:
  1. 调用Cond 中的checker函数检查是否被复制
  2. 执行notifyListAdd(), 获取 ticket,ticket 是一次 Wait 操作的唯一标识,可以用来防止重复唤醒以及保证 FIFO 式的唤醒实际该方法内部就是通过原子自增操作,对Cond 中的 notifyList 下的wait计数器进行累加
  3. 执行解锁
  4. 执行notifyListWait()将当前的goroutine挂起阻塞等待在notify队列上,收到唤醒信号之后恢复运行
  5. 然后再加锁
func (c *Cond) Wait() {
	//1.检查c是否是被复制的,如果是就panic
    c.checker.check()
    //2.wait自增1
    t := runtime_notifyListAdd(&c.notify)
    //3.解锁,注意这里必须先解锁,因为 runtime_notifyListWait 要切走 goroutine
    //所以这里要解锁,要不然其他 goroutine 没法获取到锁了,既然这里会释放锁,所以在调用Wait前,必须进行加锁
    c.L.Unlock()
    //4.将当前的goroutine挂起阻塞等待在notify队列上,收到唤醒信号之后恢复运行
    runtime_notifyListWait(&c.notify, t)
    //5.当执行到此处说明这里已经唤醒了,因此需要再度锁上
    c.L.Lock()
}
  1. 实际重点是notifyListAdd()与notifyListWait()
检查c是否被复制
type copyChecker uintptr

func (c *copyChecker) check() {
	if uintptr(*c) != uintptr(unsafe.Pointer(c)) &&
		!atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c))) &&
		uintptr(*c) != uintptr(unsafe.Pointer(c)) {
		panic("sync.Cond is copied")
	}
}
notifyListAdd()原子操作对等待队列的wait进行累加
func notifyListAdd(l *notifyList) uint32 {
 // 将wait的值原子性操作自增1,wait的初始值为0
 return atomic.Xadd(&l.wait, 1) - 1
}
notifyListWait()进行阻塞的实际方法
  1. notifyListWait中会创建一个sudog对象s,并设置s的ticket值,将它和当前的goroutine关联起来。然后加入到队尾。最后调用gopark将当前的goroutine挂起
func notifyListWait(l *notifyList, t uint32) {
 lock(&l.lock)

 // 小于notify的值的对应编号阻塞的goroutine之前已经唤醒过了,直接返回
 if less(t, l.notify) {
  unlock(&l.lock)
  return
 }

 // 获取一个sudog对象s
 s := acquireSudog()
 // 设置s中的g为当前的goroutine
 s.g = getg()
 // 设置ticket值为传入的t,可以理解为ticket与当前阻塞的goroutine(s.g)对应
 s.ticket = t
 s.releasetime = 0
 t0 := int64(0)
 if blockprofilerate > 0 {
  t0 = cputicks()
  s.releasetime = -1
 }
 // 将新创建的sudog对象s加入到队列的尾部,这个过程是在lock加锁的条件下进行的
 // 不用担心并发将s加入到l.tail冲突问题
 if l.tail == nil {
  l.head = s
 } else {
  l.tail.next = s
 }
 l.tail = s
 // 调用gopark阻塞当前的goroutine运行
 goparkunlock(&l.lock, waitReasonSyncCondWait, traceEvGoBlockCond, 3)
 if t0 != 0 {
  blockevent(s.releasetime-t0, 2)
 }
 releaseSudog(s)
}

3. Signal() 唤醒一个

  1. 唤醒等待队列中队头的goroutine,真正的实现在notifyListNotifyOne函数,此函数实现也在runtime包中的sema.go文件
func (c *Cond) Signal() {
    // 检查c是否是被复制的,如果是就panic
	c.checker.check()
	// 通知等待列表中的一个 
	runtime_notifyListNotifyOne(&c.notify)
}
  1. 查看notifyListNotifyOne(),找到队头中ticket为l.notify的对象,并将该对象关联的goroutine唤醒恢复运行
func notifyListNotifyOne(l *notifyList) {
 // 如果wait和notify值相等,说明没有阻塞等待的goroutine,也就没有要唤醒的g了,这里直接返回
 if atomic.Load(&l.wait) == atomic.Load(&l.notify) {
  return
 }
 // 加锁执行下面操作
 lock(&l.lock)

 // 加锁后再次检查wait的值跟notify是否相等,如果相等同上直接释放锁返回
 t := l.notify
 if t == atomic.Load(&l.wait) {
  unlock(&l.lock)
  return
 }

 // notify加1,相当于消费者消费一个数据(g),下面会将队列头的goroutine唤醒
 atomic.Store(&l.notify, t+1)
 // 执行循环操作,从队列中找出ticket等于notify(l.notify-1,因为此时l.notify已加1)的sudog对象
 // 从sudog对象中获取到绑定的g,然后执行readyWithTime,readyWithTime会调用goread将g唤醒
 for p, s := (*sudog)(nil), l.head; s != nil; p, s = s, s.next {
  if s.ticket == t {
   n := s.next
   if p != nil {
    p.next = n
   } else {
    l.head = n
   }
   if n == nil {
    l.tail = p
   }
   unlock(&l.lock)
   s.next = nil
   readyWithTime(s, 4)
   return
  }
 }
 // 释放锁
 unlock(&l.lock)
}

4. Broadcast() 唤醒所有

  1. 唤醒等待队列中的所有goroutine
func (c *Cond) Broadcast() {
    // 检查c是否是被复制的,如果是就panic
	c.checker.check()
	// 唤醒等待队列中所有的goroutine
	runtime_notifyListNotifyAll(&c.notify)
}
  1. notifyListNotifyAll函数也在sema.go文件,将等待队列中所有的goroutine执行goready进行唤醒。在实现的时候,通过拷贝的方法将当前链表拷贝到临时变量s中,达到了快速释放锁。这里锁的粒度比Signal还要小,处理的非常优雅
func notifyListNotifyAll(l *notifyList) {
 // 如果wait和notify值相等,说明没有阻塞等待的goroutine,也就没有要唤醒的g了,这里直接返回
 if atomic.Load(&l.wait) == atomic.Load(&l.notify) {
  return
 }

 // 加锁,将当前链表拷贝到临时变量s中,然后将原链表释放
 // 之后就可以解锁了。通过是拷贝方式达到快速解锁,这里比
 // 锁的粒度比Signal还要小。
 lock(&l.lock)
 s := l.head
 l.head = nil
 l.tail = nil

 // 原子将wait的值赋值给notify,表示[notify,wait)范围内阻塞的goroutine都将被唤醒了
 atomic.Store(&l.notify, atomic.Load(&l.wait))
 unlock(&l.lock)

 // 遍历链表中每一个sudog对象,将绑定在sudog对象上的goroutine唤醒
 for s != nil {
  next := s.next
  s.next = nil
  // readyWithTime会调用goready将goroutine唤醒
  readyWithTime(s, 4)
  s = next
 }
}

5. 问题

  1. 我们知道 sync.Cond 的底层 notifyList 是一个链表结构,我们为何不直接取链表最头部唤醒呢?为什么会有一个 ticket 机制?

这是因为 notifyList 会有乱序的可能。从我们上面 Wait 的过程可以看出,获取 ticket 和加入 notifyList,是两个独立的行为,中间会把锁释放掉。而当多个 goroutine 同时进行时,中间会产生进行并发操作,那么有可能后获取 ticket 的 goroutine,先插入到 notifyList 里面, 这就会造成 notifyList 轻微的乱序

参考博客

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值