Go sync.Cond 详解

586 阅读2分钟

1. 用法详解

sync.Cond 用于等待任务的唤醒,以跑步比赛为例,每一个运动员都是等待线程,sync.Cond对象是裁判员,所有等待运行的线程只有收到裁判员的信号后才会继续运行,其具体实现如下所示。

package main

import (
  "fmt"
  "sync"
  "time"
)

func main() {
  //3个人赛跑,1个裁判员发号施令
  cond := sync.NewCond(&sync.Mutex{})
  var wg sync.WaitGroup
  wg.Add(4) //3选手+1裁判
  for i := 1; i <= 3; i++ {
    go func(num int) {
      defer wg.Done()
      fmt.Println(num, "号选手已经就位")
      cond.L.Lock()
      cond.Wait() //等待发令枪响
      fmt.Println(num, "号选手开始跑……")
      cond.L.Unlock()
    }(i)
  }
  //等待所有goroutine都进入wait状态
  time.Sleep(2 * time.Second)
  go func() {
    defer wg.Done()
    fmt.Println("裁判:“各就各位~~预备~~”")
    fmt.Println("啪!!!")
    cond.Broadcast() //发令枪响
  }()
  //防止函数提前返回退出
  wg.Wait()
}

运行结果:

3 号选手已经就位
1 号选手已经就位
2 号选手已经就位
裁判:“各就各位~~预备~~”
啪!!!
2 号选手开始跑……
3 号选手开始跑……
1 号选手开始跑……

2. 方法与源码详解

Cond 源码

type Cond struct {
    noCopy noCopy

    // L is held while observing or changing the condition
    L Locker

    notify  notifyList
    checker copyChecker
}
  • nocopy :是在go vet工具检验时,表明 WaitGroup 结构体的具体实现是不可被复制的。
  • checker:双重检查(Double check)防止运行时拷贝
  • L:可以传入一个读写锁或互斥锁,当修改条件或者调用wait方法时需要加锁
  • notify:通知链表,调用wait()方法的Goroutine会放到这个链表中,唤醒从这里取。

copyChecker 源码

copyChecker 对比了指针本身所在的地址与指针指向的地址,来判断是否发生了拷贝。

// copyChecker holds back pointer to itself to detect object copying.
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")
 }
}

notifyList 源码

type notifyList struct {
    wait   uint32
    notify uint32
    lock   uintptr // key field of the mutex
    head   unsafe.Pointer
    tail   unsafe.Pointer
}
  • wait:下一个等待唤醒Goroutine的索引,他是在锁外自动递增的.
  • notify:下一个要通知的Goroutine的索引,他可以在锁外读取,但是只能在锁持有的情况下写入.
  • head:指向链表的头部
  • tail:指向链表的尾部

wait() 源码

func (c *Cond) Wait() {
 c.checker.check()
 t := runtime_notifyListAdd(&c.notify)
 c.L.Unlock()
 runtime_notifyListWait(&c.notify, t)
 c.L.Lock()
}
  • 执行运行期间拷贝检查,如果发生了拷贝,则直接panic程序
  • 调用runtime_notifyListAdd将等待计数器加一并解锁;
  • 调用runtime_notifyListWait等待其他 Goroutine 的唤醒并加锁

Signal() 与 Broadcast() 源码

Signal()唤醒等待队列头部的goroutine

func (c *Cond) Signal() {
 c.checker.check()
 runtime_notifyListNotifyOne(&c.notify)
}

Broadcast()唤醒等待队列所有的的goroutine

func (c *Cond) Broadcast() {
 c.checker.check()
 runtime_notifyListNotifyAll(&c.notify)
}

本文正在参加技术专题18期-聊聊Go语言框架