Go语言sync.Cond如何使用

其他教程   发布日期:2023年09月13日   浏览次数:453

本篇内容介绍了“Go语言sync.Cond如何使用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

基本使用

1 定义

  1. sync.Cond
是Go语言标准库中的一个类型,代表条件变量。条件变量是用于多个goroutine之间进行同步和互斥的一种机制。
  1. sync.Cond
可以用于等待和通知goroutine,以便它们可以在特定条件下等待或继续执行。

2 方法说明

  1. sync.Cond
的定义如下,提供了
  1. Wait
,
  1. Singal
,
  1. Broadcast
以及
  1. NewCond
方法
  1. type Cond struct {
  2. noCopy noCopy
  3. // L is held while observing or changing the condition
  4. L Locker
  5. notify notifyList
  6. checker copyChecker
  7. }
  8. func NewCond(l Locker) *Cond {}
  9. func (c *Cond) Wait() {}
  10. func (c *Cond) Signal() {}
  11. func (c *Cond) Broadcast() {}
    1. NewCond
    方法: 提供创建
    1. Cond
    实例的方法
    1. Wait
    方法: 使当前线程进入阻塞状态,等待其他协程唤醒
    1. Singal
    方法: 唤醒一个等待该条件变量的线程,如果没有线程在等待,则该方法会立即返回。
    1. Broadcast
    方法: 唤醒所有等待该条件变量的线程,如果没有线程在等待,则该方法会立即返回。

3 使用方式

当使用

  1. sync.Cond
时,通常需要以下几个步骤:
  • 定义一个互斥锁,用于保护共享数据;

  • 创建一个

    1. sync.Cond
    对象,关联这个互斥锁;
  • 在需要等待条件变量的地方,获取这个互斥锁,并使用

    1. Wait
    方法等待条件变量被通知;
  • 在需要通知等待的协程时,使用

    1. Signal
    1. Broadcast
    方法通知等待的协程。
  • 最后,释放这个互斥锁。

4 使用例子

下面是一个使用sync.Cond的简单示例,实现了一个生产者-消费者模型:

  1. var (
  2. // 1. 定义一个互斥锁
  3. mu sync.Mutex
  4. cond *sync.Cond
  5. count int
  6. )
  7. func init() {
  8. // 2.将互斥锁和sync.Cond进行关联
  9. cond = sync.NewCond(&mu)
  10. }
  11. func worker(id int) {
  12. // 消费者
  13. for {
  14. // 3. 在需要等待的地方,获取互斥锁,调用Wait方法等待被通知
  15. mu.Lock()
  16. // 这里会不断循环判断 是否有待消费的任务
  17. for count == 0 {
  18. cond.Wait() // 等待任务
  19. }
  20. count--
  21. fmt.Printf("worker %d: 处理了一个任务
  22. ", id)
  23. // 5. 最后释放锁
  24. mu.Unlock()
  25. }
  26. }
  27. func main() {
  28. // 启动5个消费者
  29. for i := 1; i <= 5; i++ {
  30. go worker(i)
  31. }
  32. for {
  33. // 生产者
  34. time.Sleep(1 * time.Second)
  35. mu.Lock()
  36. count++
  37. // 4. 在需要等待的地方,获取互斥锁,调用BroadCast/Singal方法进行通知
  38. cond.Broadcast()
  39. mu.Unlock()
  40. }
  41. }

在这个示例中,创建一个生产者在生产任务,同时创建五个消费者来消费任务。当任务数为0时,此时消费者会调用

  1. Wait
方法进入阻塞状态,等待生产者的通知。

当生产者产生任务后,使用

  1. Broadcast
方法通知所有的消费者,唤醒处于阻塞状态的消费者,开始消费任务。这里使用
  1. sync.Cond
实现多个协程之间的通信和同步。

5 为什么Sync.Cond 需要关联一个锁,然后调用Wait方法前需要先获取该锁

这里的原因在于调用

  1. Wait
方法前如果不加锁,有可能会出现竞态条件。

这里假设多个协程都处于等待状态,然后一个协程调用了Broadcast唤醒了其中一个或多个协程,此时这些协程都会被唤醒。

如下,假设调用

  1. Wait
方法前没有加锁的话,那么所有协程都会去调用
  1. condition
方法去判断是否满足条件,然后都通过验证,执行后续操作。
  1. for !condition() {
  2. c.Wait()
  3. }
  4. c.L.Lock()
  5. // 满足条件情况下,执行的逻辑
  6. c.L.Unlock()

此时会出现的情况为,本来是需要在满足

  1. condition
方法的前提下,才能执行的操作。现在有可能的效果,为前面一部分协程执行时,还是满足
  1. condition
条件的;但是后面的协程,尽管不满足
  1. condition
条件,还是执行了后续操作,可能导致程序出错。

正确的用法应该是,在调用

  1. Wait
方法前便加锁,那么即使多个协程被唤醒,一次也只会有一个协程判断是否满足
  1. condition
条件,然后执行后续操作。这样子就不会出现多个协程同时判断,导致不满足条件,也执行后续操作的情况出现。
  1. c.L.Lock()
  2. for !condition() {
  3. c.Wait()
  4. }
  5. // 满足条件情况下,执行的逻辑
  6. c.L.Unlock()

使用场景

1 基本说明

  1. sync.Cond
是为了协调多个协程之间对共享数据的访问而设计的。使用
  1. sync.Cond
的场景通常都涉及到对共享数据的操作,如果没有共享数据的操作,那么没有太大必要使用
  1. sync.Cond
来进行协调。当然,如果存在重复唤醒的场景,即使没有对共享数据的操作,也是可以使用
  1. sync.Cond
来进行协调的。

通常情况下,使用

  1. sync.Cond
的场景为:多个协程需要访问同一份共享数据,需要等待某个条件满足后才能访问或修改这份共享数据。

在这些场景下,使用

  1. sync.Cond
可以方便地实现对共享数据的协调,避免了多个协程之间的竞争和冲突,保证了共享数据的正确性和一致性。因此,如果没有涉及到共享数据的操作,就没有必要使用
  1. sync.Cond
来进行协调。

2 场景说明

2.1 同步和协调多个协程之间共享资源

下面举一个使用

  1. sync.Cond
的例子,用它来实现生产者-消费者模型。生产者往
  1. items
放置元素,当
  1. items
满了之后,便进入等待状态,等待消费者唤醒。消费者从
  1. items
中取数据,当
  1. items
空了之后,便进入等待状态,等待生产者唤醒。

这里多个协程对同一份数据进行操作,且需要基于该数据判断是否唤醒其他协程或进入阻塞状态,来实现多个协程的同步和协调。

  1. sync.Cond
就适合在这种场景下使用,其正是为这种场景设计的。
  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. "time"
  6. )
  7. type Queue struct {
  8. items []int
  9. cap int
  10. lock sync.Mutex
  11. cond *sync.Cond
  12. }
  13. func NewQueue(cap int) *Queue {
  14. q := &amp;Queue{
  15. items: make([]int, 0),
  16. cap: cap,
  17. }
  18. q.cond = sync.NewCond(&amp;q.lock)
  19. return q
  20. }
  21. func (q *Queue) Put(item int) {
  22. q.lock.Lock()
  23. defer q.lock.Unlock()
  24. for len(q.items) == q.cap {
  25. q.cond.Wait()
  26. }
  27. q.items = append(q.items, item)
  28. q.cond.Broadcast()
  29. }
  30. func (q *Queue) Get() int {
  31. q.lock.Lock()
  32. defer q.lock.Unlock()
  33. for len(q.items) == 0 {
  34. q.cond.Wait()
  35. }
  36. item := q.items[0]
  37. q.items = q.items[1:]
  38. q.cond.Broadcast()
  39. return item
  40. }
  41. func main() {
  42. q := NewQueue(10)
  43. // Producer
  44. go func() {
  45. for {
  46. q.Put(i)
  47. fmt.Printf("Producer: Put %d
  48. ", i)
  49. time.Sleep(100 * time.Millisecond)
  50. }
  51. }()
  52. // Consumer
  53. go func() {
  54. for {
  55. item := q.Get()
  56. fmt.Printf("Consumer: Get %d
  57. ", item)
  58. time.Sleep(200 * time.Millisecond)
  59. }
  60. }()
  61. wg.Wait()
  62. }

2.2 需要重复唤醒的场景中使用

在某些场景中,由于不满足某种条件,此时协程进入阻塞状态,等待条件满足后,由其他协程唤醒,再继续执行。在整个流程中,可能会多次进入阻塞状态,多次被唤醒的情况。

比如上面生产者和消费者模型的例子,生产者可能会产生一批任务,然后唤醒消费者,消费者消费完之后,会进入阻塞状态,等待下一批任务的到来。所以这个流程中,协程可能多次进入阻塞状态,然后再多次被唤醒。

  1. sync.Cond
能够实现即使协程多次进入阻塞状态,也能重复唤醒该协程。所以,当出现需要实现重复唤醒的场景时,使用
  1. sync.Cond
也是非常合适的。

原理

1 基本原理

  1. Sync.Cond
存在一个通知队列,保存了所有处于等待状态的协程。通知队列定义如下:
  1. type notifyList struct {
  2. wait uint32
  3. notify uint32
  4. lock uintptr // key field of the mutex
  5. head unsafe.Pointer
  6. tail unsafe.Pointer
  7. }

当调用

  1. Wait
方法时,此时
  1. Wait
方法会释放所持有的锁,然后将自己放到
  1. notifyList
等待队列中等待。此时会将当前协程加入到等待队列的尾部,然后进入阻塞状态。

当调用

  1. Signal
时,此时会唤醒等待队列中的第一个协程,其他继续等待。如果此时没有处于等待状态的协程,调用
  1. Signal
不会有其他作用,直接返回。当调用
  1. BoradCast
方法时,则会唤醒
  1. notfiyList
中所有处于等待状态的协程。

  1. sync.Cond
的代码实现比较简单,协程的唤醒和阻塞已经由运行时包实现了,
  1. sync.Cond
的实现直接调用了运行时包提供的API。

2 实现

2.1 Wait方法实现

  1. Wait
方法首先调用
  1. runtime_notifyListAd
方法,将自己加入到等待队列中,然后释放锁,等待其他协程的唤醒。
  1. func (c *Cond) Wait() {
  2. // 将自己放到等待队列中
  3. t := runtime_notifyListAdd(&c.notify)
  4. // 释放锁
  5. c.L.Unlock()
  6. // 等待唤醒
  7. runtime_notifyListWait(&c.notify, t)
  8. // 重新获取锁
  9. c.L.Lock()
  10. }

2.2 Singal方法实现

  1. Singal
方法调用
  1. runtime_notifyListNotifyOne
唤醒等待队列中的一个协程。
  1. func (c *Cond) Signal() {
  2. // 唤醒等待队列中的一个协程
  3. runtime_notifyListNotifyOne(&c.notify)
  4. }

2.3 Broadcast方法实现

  1. Broadcast
方法调用
  1. runtime_notifyListNotifyAll
唤醒所有处于等待状态的协程。
  1. func (c *Cond) Broadcast() {
  2. // 唤醒等待队列中所有的协程
  3. runtime_notifyListNotifyAll(&c.notify)
  4. }

使用注意事项

1 调用Wait方法前未加锁

在上面2.5已经说明了,调用

  1. Sync.Cond
方法前需要加锁,否则有可能出现竞态条件。而且,现有的
  1. sync.Cond
的实现,如果在调用
  1. Wait
方法前未加锁,此时会直接
  1. panic
,下面是一个简单例子的说明:
  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. "time"
  6. )
  7. var (
  8. count int
  9. cond *sync.Cond
  10. lk sync.Mutex
  11. )
  12. func main() {
  13. cond = sync.NewCond(&lk)
  14. wg := sync.WaitGroup{}
  15. wg.Add(2)
  16. go func() {
  17. defer wg.Done()
  18. for {
  19. time.Sleep(time.Second)
  20. count++
  21. cond.Broadcast()
  22. }
  23. }()
  24. go func() {
  25. defer wg.Done()
  26. for {
  27. time.Sleep(time.Millisecond * 500)
  28. //cond.L.Lock()
  29. for count%10 != 0 {
  30. cond.Wait()
  31. }
  32. t.Logf("count = %d", count)
  33. //cond.L.Unlock()
  34. }
  35. }()
  36. wg.Wait()
  37. }

上面代码中,协程一每隔1s,将count字段的值自增1,然后唤醒所有处于等待状态的协程。协程二执行的条件为count的值为10的倍数,此时满足执行条件,唤醒后将会继续往下执行。

但是这里在调用

  1. sync.Wait
方法前,没有先获取锁,下面是其执行结果,会抛出 fatal error: sync: unlock of unlocked mutex 错误,结果如下:
  1. count = 0
  2. fatal error: sync: unlock of unlocked mutex

因此,在调用

  1. Wait
方法前,需要先获取到与
  1. sync.Cond
关联的锁,否则会直接抛出异常。

2 Wait方法接收到通知后,未重新检查条件变量

调用

  1. sync.Wait
方法,协程进入阻塞状态后被唤醒,没有重新检查条件变量,此时有可能仍然处于不满足条件变量的场景下。然后直接执行后续操作,有可能会导致程序出错。下面举一个简单的例子:
  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. "time"
  6. )
  7. var (
  8. count int
  9. cond *sync.Cond
  10. lk sync.Mutex
  11. )
  12. func main() {
  13. cond = sync.NewCond(&lk)
  14. wg := sync.WaitGroup{}
  15. wg.Add(3)
  16. go func() {
  17. defer wg.Done()
  18. for {
  19. time.Sleep(time.Second)
  20. cond.L.Lock()
  21. // 将flag 设置为true
  22. flag = true
  23. // 唤醒所有处于等待状态的协程
  24. cond.Broadcast()
  25. cond.L.Unlock()
  26. }
  27. }()
  28. for i := 0; i < 2; i++ {
  29. go func(i int) {
  30. defer wg.Done()
  31. for {
  32. time.Sleep(time.Millisecond * 500)
  33. cond.L.Lock()
  34. // 不满足条件,此时进入等待状态
  35. if !flag {
  36. cond.Wait()
  37. }
  38. // 被唤醒后,此时可能仍然不满足条件
  39. fmt.Printf("协程 %d flag = %t", i, flag)
  40. flag = false
  41. cond.L.Unlock()
  42. }
  43. }(i)
  44. }
  45. wg.Wait()
  46. }

在这个例子,我们启动了一个协程,定时将

  1. flag
设置为true,相当于每隔一段时间,便满足执行条件,然后唤醒所有处于等待状态的协程。

然后又启动了两个协程,在满足条件的前提下,开始执行后续操作,但是这里协程被唤醒后,没有重新检查条件变量,具体看第39行。这里会出现的场景是,第一个协程被唤醒后,此时执行后续操作,然后将

  1. flag
重新设置为false,此时已经不满足条件了。之后第二个协程唤醒后,获取到锁,没有重新检查此时是否满足执行条件,直接向下执行,这个就和我们预期不符,可能会导致程序出错,代码执行效果如下:

协程 1 flag = true
协程 0 flag = false
协程 1 flag = true
协程 0 flag = false

可以看到,此时协程0执行时,

  1. flag
的值均为
  1. false
,说明此时其实并不符合执行条件,可能会导致程序出错。因此正确用法应该像下面这样子,被唤醒后,需要重新检查条件变量,满足条件之后才能继续向下执行。
  1. c.L.Lock()
  2. // 唤醒后,重新检查条件变量是否满足条件
  3. for !condition() {
  4. c.Wait()
  5. }
  6. // 满足条件情况下,执行的逻辑
  7. c.L.Unlock()

以上就是Go语言sync.Cond如何使用的详细内容,更多关于Go语言sync.Cond如何使用的资料请关注九品源码其它相关文章!