条件变量与互斥锁配合使用
条件变量是基于互斥锁的。用于协调想要访问共享资源的线程。当共享资源状态发生变化,用来通知被互斥锁阻塞的线程
。- 条件变量使用互斥锁初始化、并提供了三个方法:等待通知(Wait),单发通知(Signal)和广播通知(Broadcast)。
- sync.Cond类型不是开箱即用的,需要通过构造函数NewCond创建它的指针值,NewCond需要一个sync.Locker类型的参数。
var mailbox uint8
var lock sync.Mutex
sendCond := sync.NewCond(&lock)
recvCond := sync.NewCond(&lock)
- 调用Wait方法需要在互斥锁保护下进行;Signal方法和Broadcast方法需要在互斥锁解锁后调用。
- 发送方代码:
lock.Lock()
for mailbox == 1 {
sendCond.Wait()
}
mailbox = 1
lock.Unlock()
recvCond.Signal()
lock.Lock()
for mailbox == 0 {
recvCond.Wait()
}
mailbox = 0
lock.Unlock()
sendCond.Signal()
Wait方法
- 把当前goroutine加入到当前条件变量的通知等待队列。
- 解锁当前条件变量基于的互斥锁。
- 让当前goroutine处于等待状态,等到通知到来再决定是否唤醒。此时goroutine阻塞在Wait()这一行。
- 通知到来并决定唤醒,那么唤醒之后重新锁定当前条件变量基于的互斥锁。
lock加锁 -> wait解锁等通知 -> 通知到来并唤醒 -> wait加锁 -> lock解锁
为什么要先加锁,再调用Wait
- 因为Wait方法中会解锁:
如果不加锁直接调用Wait,相当于直接解锁,会引发fatal error!
- 为什么Wait方法中要解锁:
如果不解锁,在互斥锁锁定情况下,当前goroutine阻塞,而其他goroutine都无法进入临界区(因为他们都拿不到锁),所以没有一个goroutine能够去修改共享资源的状态
。放到上面的场景中就是,发送方加了互斥锁,如果Wait不解锁,那么接收方只能等最后发送方自己解锁了再去拿。
为什么要用for而不是if
- 主要为了保险,可能存在以下情况:一个goroutine收到通知被唤醒,却发现共享资源依然不符合要求(第一次不符合要求就Wait了),那么应该继续等待下一次通知。
- 多个goroutine关注同一个共享状态,比如有多个发信方,应该都等待发信,直到接收方把信取走。但是如果使用if,则发信方最多等待一次,就纷纷设置了mailbox=1然后退出了。造成的结果是,大家都发了,接收方只收到一封。
- 共享资源的状态可能是多个,比如mailbox可能值是0,1,2,由于每次状态改变后结果只有一个。那么可能当前goroutine被唤醒,但是共享资源状态不是我想要的,那我就继续等待。
- 还有一种可能,操作系统可能会在没有收到条件变量通知的情况下唤醒goroutine,如果没有循环,可能就出问题。
Signal方法和Broadcast方法
- Signal只会唤醒一个等待的goroutine,Broadcast会唤醒所有。
- Wait方法会将当前goroutine加入到通知队列队尾,而Signal方法总会从通知队列的队首开始,所以通常唤醒的是最早开始等待的。
- 在发送通知时,如果没有等待的goroutine,该通知就没有任何用处。