开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 21 天,点击查看活动详情
Cond
在 Go 语言中,cond 是 sync 标准库包中的一个类型,表示一个条件变量,它可以在多个 goroutine 之间进行信号通信和数据传递。通常,它与 sync.Mutex 配合使用,用于在共享资源上等待和通知其他 goroutine 的状态变化。
Cond 类型包含以下方法:
func (c *Cond) Broadcast():唤醒所有等待的 goroutine,让它们尝试重新获取锁。func (c *Cond) Signal():唤醒一个等待的 goroutine,让它尝试重新获取锁。func (c *Cond) Wait():等待通知并重新获取锁。在调用此方法之前必须先锁定c.L。
在使用 Cond 的时候,一般需要遵循如下的模式:
- 加锁
- 检查等待条件
- 如果等待条件不满足,调用
Cond.Wait()等待通知 - 收到通知后,重新检查等待条件
- 如果等待条件满足,执行操作并解锁
- 如果等待条件不满足,返回到第 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。可以看到这里保证了等待条件读取的原子性,防止并发带来的问题。