Go之sync.Cond
Posted siwluxuefeng
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go之sync.Cond相关的知识,希望对你有一定的参考价值。
sync.Cond
条件变量是基于互斥锁的,条件变量并不是被用来保护临界区和共享资源的,它是用于协调想要访问共享资源的那些线程的。当共享资源的状态发生变化时,它可以被用来通知被互斥锁阻塞的线程。
条件变量的初始化离不开互斥锁,并且它的方法有的也是基于互斥锁的。
条件变量提供的方法有三个:
等待通知(wait)、在互斥锁下进行;
单发通知(signal)、在互斥锁解锁后进行;
广播通知(broadcast)、在互斥锁解锁后进行;
var mailbox uint8
var lock sync.RWMutex
sendCond := sync.NewCond(&lock)
recvCond := sync.NewCond(lock.RLocker())
变量mailbox代表信箱,是uint8类型的。 若它的值为0则表示信箱中没有情报,而当它的值为1时则说明信箱中有情报。lock是一个类型为sync.RWMutex的变量,是一个读写锁,也可以被视为信箱上的那把锁。
另外,基于这把锁,创建了两个代表条件变量的变量,名字分别叫sendCond和recvCond。 它们都是*sync.Cond类型的,同时也都是由sync.NewCond函数来初始化的。
与sync.Mutex类型和sync.RWMutex类型不同,sync.Cond类型并不是开箱即用的。我们只能利用sync.NewCond函数创建它的指针值。这个函数需要一个sync.Locker类型的参数值。
条件变量是基于互斥锁的,它必须有互斥锁的支撑才能够起作用。因此,这里的参数值是不可或缺的,它会参与到条件变量的方法实现当中。
sync.Locker其实是一个接口,在它的声明中只包含了两个方法定义,即:Lock()和Unlock()。
sync.Mutex类型和sync.RWMutex类型都拥有Lock方法和Unlock方法,只不过它们都是指针方法。因此,这两个类型的指针类型才是sync.Locker接口的实现类型。
在为sendCond变量做初始化的时候,把基于lock变量的指针值传给了sync.NewCond函数。原因是,lock变量的Lock方法和Unlock方法分别用于对其中写锁的锁定和解锁,它们与sendCond变量的含义是对应的。sendCond是专门为放置情报而准备的条件变量,向信箱里放置情报,可以被视为对共享资源的写操作。
recvCond变量代表的是专门为获取情报而准备的条件变量。 虽然获取情报也会涉及对信箱状态的改变,但是好在做这件事的人只会有你一个,而且我们也需要借此了解一下,条件变量与读写锁中的读锁的联用方式。所以,在这里,我们暂且把获取情报看做是对共享资源的读操作。
因此,为了初始化recvCond这个条件变量,我们需要的是lock变量中的读锁,并且还需要是sync.Locker类型的。
可是,lock变量中用于对读锁进行锁定和解锁的方法却是RLock和RUnlock,它们与sync.Locker接口中定义的方法并不匹配。sync.RWMutex类型的RLocker方法可以实现这一需求。我们只要在调用sync.NewCond函数时,传入调用表达式lock.RLocker()的结果值,就可以使该函数返回符合要求的条件变量了。
为什么说通过lock.RLocker()得来的值就是lock变量中的读锁呢?实际上,这个值所拥有的Lock方法和Unlock方法,在其内部会分别调用lock变量的RLock方法和RUnlock方法。也就是说,前两个方法仅仅是后两个方法的代理而已。
我们现在有四个变量。一个是代表信箱的mailbox,一个是代表信箱上的锁的lock。还有两个是,代表了蓝帽子小孩儿的sendCond,以及代表了红帽子小孩儿的recvCond。
条件变量的Wait方法做了什么?
条件变量的Wait方法主要做了四件事。
1.把调用它的 goroutine(也就是当前的 goroutine)加入到当前条件变量的通知队列中。
2.解锁当前的条件变量基于的那个互斥锁。
3.让当前的 goroutine 处于等待状态,等到通知到来时再决定是否唤醒它。此时,这个 goroutine 就会阻塞在调用这个Wait方法的那行代码上。
4.如果通知到来并且决定唤醒这个 goroutine,那么就在唤醒它之后重新锁定当前条件变量基于的互斥锁。自此之后,当前的 goroutine 就会继续执行后面的代码了。
因为条件变量的Wait方法在阻塞当前的 goroutine 之前,会解锁它基于的互斥锁,所以在调用该Wait方法之前,我们必须先锁定那个互斥锁,否则在调用这个Wait方法时,就会引发一个不可恢复的 panic。
为什么条件变量的Wait方法要这么做呢?你可以想象一下,如果Wait方法在互斥锁已经锁定的情况下,阻塞了当前的 goroutine,那么又由谁来解锁呢?别的 goroutine 吗?
先不说这违背了互斥锁的重要使用原则,即:成对的锁定和解锁,就算别的 goroutine 可以来解锁,那万一解锁重复了怎么办?由此引发的 panic 可是无法恢复的。
如果当前的 goroutine 无法解锁,别的 goroutine 也都不来解锁,那么又由谁来进入临界区,并改变共享资源的状态呢?只要共享资源的状态不变,即使当前的 goroutine 因收到通知而被唤醒,也依然会再次执行这个Wait方法,并再次被阻塞。
所以说,如果条件变量的Wait方法不先解锁互斥锁的话,那么就只会造成两种后果:不是当前的程序因 panic 而崩溃,就是相关的 goroutine 全面阻塞。
if语句只会对共享资源的状态检查一次,而for语句却可以做多次检查,直到这个状态改变为止。那为什么要做多次检查呢?
这主要是为了保险起见。如果一个 goroutine 因收到通知而被唤醒,但却发现共享资源的状态,依然不符合它的要求,那么就应该再次调用条件变量的Wait方法,并继续等待下次通知的到来。
有多个 goroutine 在等待共享资源的同一种状态。比如,它们都在等mailbox变量的值不为0的时候再把它的值变为0,这就相当于有多个人在等着我向信箱里放置情报。虽然等待的 goroutine 有多个,但每次成功的 goroutine 却只可能有一个。条件变量的Wait方法会在当前的 goroutine 醒来后先重新锁定那个互斥锁。在成功的 goroutine 最终解锁互斥锁之后,其他的 goroutine 会先后进入临界区,但它们会发现共享资源的状态依然不是它们想要的。这个时候,for循环就很有必要了。
共享资源可能有的状态不是两个,而是更多。比如,mailbox变量的可能值不只有0和1,还有2、3、4。这种情况下,由于状态在每次改变后的结果只可能有一个,所以,在设计合理的前提下,单一的结果一定不可能满足所有 goroutine 的条件。那些未被满足的 goroutine 显然还需要继续等待和检查。
有一种可能,共享资源的状态只有两个,并且每种状态都只有一个 goroutine 在关注,就像我们在主问题当中实现的那个例子那样。不过,即使是这样,使用for语句仍然是有必要的。原因是,在一些多 CPU 核心的计算机系统中,即使没有收到条件变量的通知,调用其Wait方法的 goroutine 也是有可能被唤醒的。这是由计算机硬件层面决定的,即使是操作系统(比如 Linux)本身提供的条件变量也会如此。
综上所述,在包裹条件变量的Wait方法的时候,我们总是应该使用for语句。
条件变量的Signal方法和Broadcast方法有哪些异同?
条件变量的Signal方法和Broadcast方法都是被用来发送通知的,不同的是,前者的通知只会唤醒一个因此而等待的 goroutine,而后者的通知却会唤醒所有为此等待的 goroutine。
条件变量的Wait方法总会把当前的 goroutine 添加到通知队列的队尾,而它的Signal方法总会从通知队列的队首开始,查找可被唤醒的 goroutine。所以,因Signal方法的通知,而被唤醒的 goroutine 一般都是最早等待的那一个。
这两个方法的行为决定了它们的适用场景。如果你确定只有一个 goroutine 在等待通知,或者只需唤醒任意一个 goroutine 就可以满足要求,那么使用条件变量的Signal方法就好了。
否则,使用Broadcast方法总没错,只要你设置好各个 goroutine 所期望的共享资源状态就可以了。
此外,再次强调一下,与Wait方法不同,条件变量的Signal方法和Broadcast方法并不需要在互斥锁的保护下执行。恰恰相反,我们最好在解锁条件变量基于的那个互斥锁之后,再去调用它的这两个方法。这更有利于程序的运行效率。
请注意,条件变量的通知具有即时性。也就是说,如果发送通知的时候没有 goroutine 为此等待,那么该通知就会被直接丢弃。在这之后才开始等待的 goroutine 只可能被后面的通知唤醒。
package sync
import (
"sync/atomic"
"unsafe"
)
// Cond implements a condition variable, a rendezvous point
// for goroutines waiting for or announcing the occurrence of an event.
// Each Cond has an associated Locker L (often a *Mutex or *RWMutex), which must be held when changing the condition and when calling the Wait method.
type Cond struct
noCopy noCopy
// L is held while observing or changing the condition
L Locker
notify notifyList
checker copyChecker
// NewCond returns a new Cond with Locker l.
func NewCond(l Locker) *Cond
return &CondL: l
func (c *Cond) Wait()
c.checker.check()
t := runtime_notifyListAdd(&c.notify)
c.L.Unlock()
runtime_notifyListWait(&c.notify, t)
c.L.Lock()
// Signal wakes one goroutine waiting on c, if there is any.
// It is allowed but not required for the caller to hold c.L during the call.
func (c *Cond) Signal()
c.checker.check()
runtime_notifyListNotifyOne(&c.notify)
// Broadcast wakes all goroutines waiting on c.
// It is allowed but not required for the caller to hold c.L during the call.
func (c *Cond) Broadcast()
c.checker.check()
runtime_notifyListNotifyAll(&c.notify)
// 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")
type noCopy struct
// Lock is a no-op used by -copylocks checker from `go vet`.
func (*noCopy) Lock()
以上是关于Go之sync.Cond的主要内容,如果未能解决你的问题,请参考以下文章