【Go并发编程】Cond源码阅读

236 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 21 天,点击查看活动详情

Cond

在 Go 语言中,condsync 标准库包中的一个类型,表示一个条件变量,它可以在多个 goroutine 之间进行信号通信和数据传递。通常,它与 sync.Mutex 配合使用,用于在共享资源上等待和通知其他 goroutine 的状态变化。

Cond 类型包含以下方法:

  • func (c *Cond) Broadcast():唤醒所有等待的 goroutine,让它们尝试重新获取锁。
  • func (c *Cond) Signal():唤醒一个等待的 goroutine,让它尝试重新获取锁。
  • func (c *Cond) Wait():等待通知并重新获取锁。在调用此方法之前必须先锁定 c.L

在使用 Cond 的时候,一般需要遵循如下的模式:

  1. 加锁
  2. 检查等待条件
  3. 如果等待条件不满足,调用 Cond.Wait() 等待通知
  4. 收到通知后,重新检查等待条件
  5. 如果等待条件满足,执行操作并解锁
  6. 如果等待条件不满足,返回到第 3 步

以下是一个简单的使用 Cond 的示例,实现了一个并发安全的消息队列

type MessageQueue struct {
    messages []string
    cond     *sync.Cond
}

func NewMessageQueue() *MessageQueue {
    mq := &MessageQueue{cond: sync.NewCond(&sync.Mutex{})}
    go func() {
        for {
            mq.L.Lock()
            for len(mq.messages) == 0 {
                mq.cond.Wait()
            }
            message := mq.messages[0]
            mq.messages = mq.messages[1:]
            mq.L.Unlock()
            fmt.Println(message)
        }
    }()
    return mq
}

func (mq *MessageQueue) Put(message string) {
    mq.L.Lock()
    mq.messages = append(mq.messages, message)
    mq.L.Unlock()
    mq.cond.Signal()
}

在这个例子中,当队列为空时,消费者调用 cond.Wait() 进入等待状态,直到收到一个信号。当生产者向队列中添加消息时,它调用 cond.Signal() 来通知一个等待的消费者。

第二个例子是,每个运动员准备好就通知一下裁判,当10个运动员全部准备好才开始比赛:

func main() {
    c := sync.NewCond(&sync.Mutex{})
    var ready int

    for i := 0; i < 10; i++ {
        go func(i int) {
            time.Sleep(time.Duration(rand.Int63n(10)) * time.Second)

            // 加锁更改等待条件
            c.L.Lock()
            ready++
            c.L.Unlock()

            log.Printf("运动员#%d 已准备就绪\n", i)
            // 广播唤醒所有的等待者
            c.Broadcast()
        }(i)
    }

    c.L.Lock()
    // for ready != 10 {
    c.Wait()
    log.Println("裁判员被唤醒一次")
    // }
    c.L.Unlock()

    //所有的运动员是否就绪
    log.Println("所有运动员都准备就绪。比赛开始,3,2,1, ......")
}

源码阅读

type notifyList struct {
   wait   uint32
   notify uint32
   lock   uintptr // key field of the mutex
   head   unsafe.Pointer
   tail   unsafe.Pointer
}

type copyChecker uintptr

type Cond struct {
   noCopy  noCopy
   L       Locker
   notify  notifyList
   checker copyChecker
}
  • L是一个锁
  • notify 是等待队列
  • checker 检测是否复制
func NewCond(l Locker) *Cond {
   return &Cond{L: l}
}

func (c *Cond) Wait() {
   // 在运行时检查 Cond 是否被复制使用
   c.checker.check()
   // 加入到等待队列
   t := runtime_notifyListAdd(&c.notify)
   c.L.Unlock()
   // 进入阻塞
   runtime_notifyListWait(&c.notify, t)
   // 唤醒 
   c.L.Lock()
}

func (c *Cond) Signal() {
   c.checker.check()
   // 唤醒一个
   runtime_notifyListNotifyOne(&c.notify)
}

func (c *Cond) Broadcast() {
   c.checker.check()
   // 唤醒全部的
   runtime_notifyListNotifyAll(&c.notify)
}

type copyChecker uintptr
// 在运行时检查 Cond 是否被复制使用
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")
   }
}

首先要强调一下,官方要求的wait使用方法:

c.L.Lock()
for !condition() {
    c.Wait()
}
... make use of condition ...
c.L.Unlock()

直接看,好像有一个gorouting wait后,其他gorouting都在等锁,其实不是的,看Wait方法在加入到等待队列后就unlock了,这是为了保证加入队列的原子性。unlock后就阻塞了,其他gorouting可以再进入Wait等待队列。当唤醒时,先lock,如果不符合条件再次阻塞,符合条件就往后执行unlock。可以看到这里保证了等待条件读取的原子性,防止并发带来的问题。