Cond的文档很好的描述了其存在的目的:

Cond实现了一个条件变量,用于等待或宣布事件发生时goroutine的交汇点。

在这个定义中,“事件”是指两个或更多的goroutine之间的任何信号,仅指事件发生了,不包含其他任何信息。 通常,你可能想要在收到某个goroutine信号前令其处于等待状态。 如果我们要在不使用Cond的情况下实现这一点,那么一个粗暴的方法就是使用无限循环:

for conditionTrue() == false {
}

然而这会导致消耗一个内核的所有周期。我们可以引入time.sleep来改善这一点:

for conditionTrue() == false {
    time.Sleep(1 * time.Millisecond)
}

这样就看起来好点了,但执行效率依然很低效,而且你需要显示标明需要休眠多久:太长或太短都会不必要的消耗无谓的CPU时间。如果有一种方法可以让goroutine有效地睡眠,直到唤醒并检查其状态,那将会更好。这种需求简直是为Cond量身定制的,使用它我们可以这样改造上面的例子:

c := sync.NewCond(&sync.Mutex{}) // 1
c.L.Lock() // 2
for conditionTrue() == false {
    c.Wait() // 3
}
c.L.Unlock() // 4
  1. 这里我们实例化一个新的Cond。NewCond函数传入的参数实现了sync.Locker类型。Cond类型允许以并行安全的方式与其他goroutines协调。
  2. 在这里我们进行锁定。这一步很必要,因为Wait的调用会执行解锁并暂停该goroutine。
  3. 在这里我们进入暂停状态,这是阻塞的,直到接收到通知。
  4. 这里执行解锁,这一步很必要,因为当调用退出时,它会c.L上调用Lock。

这个例子相对之前的效率就比较高了。请注意,对Wait的调用不仅仅是阻塞,它暂停当前的goroutine,允许其他goroutine在操作系统线程上运行。当你调用Wait时,还会发生其他一些事情:进入Wait后,Cond的变量Locker将调用Unlock,并在退出Wait时,Cond变量的Locker上会调用Lock。 在我看来,这有点让人不习惯; 这实际上是该方法的隐藏副作用。 看起来我们在等待条件发生的整个过程中都持有这个锁,但事实并非如此。 当你检查代码时,需要留意这一点。

让我们扩展这个例子,来看看等待信号的goroutine和发送信号的goroutine该怎么写。假设我们有一个固定长度为2的队列,并且我们要将10个元素放入队列中。 我们希望一有空间就能放入,所以在队列中有空间时需要立刻通知:

c := sync.NewCond(&sync.Mutex{})    //1
queue := make([]interface{}, 0, 10) //2

removeFromQueue := func(delay time.Duration) {
    time.Sleep(delay)
    c.L.Lock()        //8
    queue = queue[1:] //9
    fmt.Println("Removed from queue")
    c.L.Unlock() //10
    c.Signal()   //11
}

for i := 0; i < 10; i++ {
    c.L.Lock() //3
    for len(queue) == 2 { //4
        c.Wait() //5
    }
    fmt.Println("Adding to queue")
    queue = append(queue, struct{}{})
    go removeFromQueue(1 * time.Second) //6
    c.L.Unlock()                        //7
}
  1. 首先,我们使用一个标准的sync.Mutex作为Locker来创建Cond。
  2. 接下来,我们创建一个长度为零的切片。 由于我们知道最终会添加10个元素,因此我们将其容量设为10。
  3. 在进入关键的部分前调用Lock来锁定c.L。
  4. 在这里我们检查队列的长度,以确认什么时候需要等待。由于removeFromQueue是异步的,for不满足时才会跳出,而if做不到重复判断,这一点很重要。
  5. 调用Wait,这将阻塞main goroutine,直到接受到信号。
  6. 这里我们创建一个新的goroutine,它会在1秒后将元素移出队列。
  7. 这里我们退出条件的关键部分,因为我们已经成功加入了一个元素。
  8. 我们再次进入该并发条件下的关键部分,以修改与并发条件判断直接相关的数据。
  9. 在这里,我们移除切片的头部并重新分配给第二个元素,这一步模拟了元素出列。
  10. 我们退出操作关键部分,因为我们已经成功移除了一个元素。
  11. 这里,我们发出信号,通知处于等待状态的goroutine可以进行下一步了。

这会输出:

Adding to queue Adding to queue Removed from queue Adding to queue Removed from queue Adding to queue Removed from queue Adding to queue Removed from queue Adding to queue Removed from queue Adding to queue Removed from queue Adding to queue Removed from queue Adding to queue Removed from queue Adding to queue

强烈建议将代码中for换成if感受下,作者考虑的真周全

正如你所看到的,程序成功地将所有10个元素添加到队列中(并且在它有机会在最后两项出队之前退出)。 它也会持续等待,直到至少有一个元素在放入另一个元素之前出列。

在这个例子中,我们使用了一个新的方法,Signal。这是Cond类型提供的两种通知方法之一,用于通知在等待调用上阻塞的goroutines条件已被触发。另一种方法是Broadcast。在内部,运行时维护一个等待信号发送的goroutines的FIFO列表; Signal寻找等待时间最长的goroutine并通知,而Broadcast向所有处在等待状态的goroutine发送信号。Broadcast可以说是两种方法中最有趣的方式,因为它提供了一种同时与多个goroutine进行通信的解决方案。 我们可以通过通道轻松地再现Signal(随后我们会看到这个例子),但是再现对Broadcast重复呼叫的行为将很困难。另外,Cond类型比使用通道更高效。

为了了解Broadcast是如何使用的,假设我们正在创建一个带有按钮的GUI程序,该程序需要注册任意数量的函数,当点击按钮时运行这些函数。可以使用Cond的Brocast来通知所有已注册函数,让我们看看该如何实现:

type Button struct {
    //1
    Clicked *sync.Cond
}
button := Button{Clicked: sync.NewCond(&sync.Mutex{})}

subscribe := func(c *sync.Cond, fn func()) { //2
    var tempwg sync.WaitGroup
    tempwg.Add(1)
    go func() {
        tempwg.Done()
        c.L.Lock()
        defer c.L.Unlock()
        c.Wait()
        fn()
    }()
    tempwg.Wait()
}

var wg sync.WaitGroup //3
wg.Add(3)
subscribe(button.Clicked, func() { //4
    fmt.Println("Maximizing window.")
    wg.Done()
})
subscribe(button.Clicked, func() { //5
    fmt.Println("Displaying annoying dialog box!")
    wg.Done()
})
subscribe(button.Clicked, func() { //6
    fmt.Println("Mouse clicked.")
    wg.Done()
})

button.Clicked.Broadcast() //7

wg.Wait()
  1. 我们定义一个Button类型,包含了sync.Cond指针类型的Clicked属性,这是goroutine接收通知的关键条件。
  2. 这里我们定义了一个较为简单的函数,它允许我们注册函数来处理信号。每个注册的函数都在自己的goroutine上运行,并且在该goroutine不会退出,直到接收到通知。
  3. 在这里,我们为按钮点击设置了一个处理程序。 它反过来在Clicked Cond上调用Broad cast以让所有注册函数知道按钮已被点击。
  4. 这里我们创建一个WaitGroup。 这只是为了确保我们的程序在写入标准输出之前不会退出。
  5. 在这里我们注册一个处理函数,模拟点击时最大化窗口。
  6. 在这里我们注册一个处理函数,模拟点击时显示对话框。
  7. 接下来,我们模拟按钮被点击。

这会输出:

Mouse clicked.
Maximizing window.
Displaying annoying dialog box! 

可以看到,通过调用Broadcast,三个处理函数都运行了。如果不是wg WaitGroup,我们可以多次调button.Clicked.Broadcast(),并且每次都将运行这三个处理函数。 这是通道难以做到的,也是使用Cond类型的优势之一。

最后编辑: kuteng  文档更新时间: 2021-01-02 17:30   作者:kuteng