sync.Cond 的用法与原理解析

183 阅读3分钟

背景

sync.Cond 是 Go 语言中的一个同步原语,用于实现多个 goroutine 之间的条件同步。它可以让一个或多个 goroutine 等待某个条件满足,而另一个 goroutine 在条件满足时可以通知它们继续执行。

sync.Cond 的主要应用场景有:

  • 并发访问数据库或共享内存时的同步
  • 等待特定任务完成后的同步
  • 控制并发 goroutine 数量

用法

使用 sync.Cond 的基本步骤如下:

  1. 创建一个 sync.Cond 实例,通常需要传入一个 locker 对象,如 mutex。

  2. 在需要等待条件的 goroutine 中,调用 cond.Wait() 方法,该 goroutine 会阻塞在这里。

  3. 在条件满足时,在另一个 goroutine 中调用 cond.Signal() 方法,通知等待的 goroutine 唤醒继续执行。

  4. 被 Notify 的 goroutine 从 Wait() 返回后就可以继续往下执行了。

一个简单的示例:

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

// 共享队列
var queue = make([]int, 0)

// 队列最大容量
const queueCap = 5

var mutex sync.Mutex
var cond = sync.NewCond(&mutex)

// 生产者: 生成随机数放入队列
func producer() {
	for {
		mutex.Lock()
		for len(queue) == queueCap {
			cond.Wait()
		}

		randNum := rand.Intn(100)
		queue = append(queue, randNum)
		fmt.Println("Produce:", randNum)

		mutex.Unlock()
		cond.Signal()
		time.Sleep(time.Second)
	}
}

// 消费者:从队列中取出数据
func consumer() {
	for {
		mutex.Lock()
		for len(queue) == 0 {
			cond.Wait()
		}

		num := queue[0]
		queue = queue[1:]
		fmt.Println("Consume:", num)

		mutex.Unlock()
		cond.Signal()
		time.Sleep(time.Second)
	}
}

func main() {
	go producer()
	go consumer()

	time.Sleep(10 * time.Second)
}

需要注意:

  • cond.Wait() 需要在锁保护下调用,并在返回前解锁,避免死锁。
  • 同一个 Cond 可以重复使用,但尽量避免信号丢失的情况。
  • 可以通过 Broadcast() 一次性唤醒所有等待的 goroutine。

原理

sync.Cond 的核心是维护一个等待条件的 goroutine 计数器。 等待条件的 goroutine 通过 Wait() 使计数器加 1,并阻塞自身。 另一个 goroutine 通过 Signal() 使计数器减 1,从而唤醒被阻塞的 goroutine。

具体实现:

  • Cond 在创建时会初始化一个计数器 notifyList。

  • Wait() 将当前 goroutine 加入 notifyList,并解锁等待条件满足。

  • Signal() 将 notifyList 中的一个 goroutine 唤醒。

  • Broadcast() 将 notifyList 中的所有 goroutine 唤醒。

  • 当计数器为 0 时,会自动广播唤醒。

这样通过计数器和阻塞的方式实现了条件变量的语义,使得多个 goroutine 可以进行同步。

详情请看源码:

package sync
 
import (
	"sync/atomic"
	"unsafe"
)
 
type Cond struct {
	// L 是在观察或更改条件时持有的锁。
	L Locker
	// notify 是一个通知列表,用于唤醒等待条件的 goroutine。
	notify notifyList
	// checker 是一个复制检查器,用于检测 Cond 对象是否被复制。
	checker copyChecker
}
 
// NewCond 返回一个新的 Cond 实例,其中 Locker 是 Cond 实例的写锁。
func NewCond(l Locker) *Cond {
	return &Cond{
		L: l,
	}
}
 
// Wait 使一个 goroutine 等待条件满足。
func (c *Cond) Wait() {
	c.checker.check()
	t := runtime_notifyListAdd(&c.notify)
	c.L.Unlock()
	runtime_notifyListWait(&c.notify, t)
	c.L.Lock()
}
 
// 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)
}
 
// copyChecker 持有一个指向自身的指针,用于检测 Cond 对象是否被复制。
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")
	}
}
 
type noCopy struct{}
 
// Lock 是一个空操作,用于 -copylocks 检查器从 'go vet' 中检查 Cond 对象是否被复制。
func (*noCopy) Lock()   {}
func (*noCopy) Unlock() {}

小结

sync.Cond 是 Go 中实现条件变量同步的利器,使用相对简单,通过计数器和阻塞的方式实现了条件同步的语义。在并发程序中如果需要等待某条件才能继续执行,可以考虑使用 sync.Cond。