深入学习Golang并发编程必备利器之sync.Cond类型

 更新时间:2023年05月04日 09:15:48   作者:金刀大菜牙  
Go 语言的 sync 包提供了一系列同步原语,其中 sync.Cond 就是其中之一。本文将深入探讨 sync.Cond 的实现原理和使用方法,帮助大家更好地理解和应用 sync.Cond,需要的可以参考一下

Go 语言的 sync 包提供了一系列同步原语,其中 sync.Cond 就是其中之一。sync.Cond 的作用是在多个 goroutine 之间进行条件变量的同步。本文将深入探讨 sync.Cond 的实现原理和使用方法,帮助大家更好地理解和应用 sync.Cond。

1. sync.Cond 的基本概念

1.1 条件变量

条件变量是一种同步机制,用于在多个 goroutine 之间进行同步。条件变量通常是和互斥锁一起使用的,用于等待某个条件的出现。

在 Go 语言中,条件变量由 sync.Cond 类型实现。它提供了两个主要的方法:Wait 和 Signal/Broadcast。Wait 方法用于等待条件变量的出现,Signal/Broadcast 方法用于通知等待中的 goroutine。

1.2 互斥锁

互斥锁是一种用于控制对共享资源访问的同步机制。它能够保证同一时刻只有一个 goroutine 能够访问共享资源。

在 Go 语言中,互斥锁由 sync.Mutex 类型实现。它提供了两个主要的方法:Lock 和 Unlock。Lock 方法用于加锁,保证同一时刻只有一个 goroutine 能够访问共享资源;Unlock 方法用于解锁,允许其他 goroutine 访问共享资源。

1.3 条件变量的实现原理

条件变量的实现原理基于互斥锁和 goroutine 队列。

假设有一个条件变量 cond,初始时它没有被触发。当一个 goroutine 调用 cond.Wait() 方法时,它会加锁并将自己加入到 cond 的 goroutine 队列中。接着,它会解锁并进入睡眠状态,等待被唤醒。

当另一个 goroutine 调用 cond.Signal() 或者 cond.Broadcast() 方法时,它会重新加锁,并从 cond 的 goroutine 队列中选择一个 goroutine 唤醒。被唤醒的 goroutine 会重新加锁,然后继续执行。

需要注意的是,被唤醒的 goroutine 并不会立即执行,它会等待重新获得锁之后才会继续执行。

2. sync.Cond 的基本用法

2.1 创建 sync.Cond 对象

sync.Cond 对象需要依赖一个 sync.Mutex 或 sync.RWMutex 对象来进行同步和互斥操作。我们可以使用 sync.NewCond 方法来创建一个新的 sync.Cond 对象,该方法接受一个 Mutex 或 RWMutex 对象作为参数,返回一个对应的条件变量对象。

 package main
 ​
 import (
     "fmt"
     "sync"
 )
 ​
 func main() {
     var mu sync.Mutex
     cond := sync.NewCond(&mu)
 ​
     // ...
 }

2.2 等待条件变量

sync.Cond 提供了 Wait 方法来等待条件变量的信号。Wait 方法需要在持有 Mutex 或 RWMutex 的情况下进行调用,否则会抛出 panic 异常。

 func (c *Cond) Wait()

Wait 方法将当前 goroutine 暂停,等待条件变量的信号。在等待过程中,Mutex 或 RWMutex 将被释放,其他 goroutine 可以获取锁并修改共享变量,但是当前 goroutine 仍然保持在等待队列中,直到收到唤醒信号。当 Wait 方法返回时,Mutex 或 RWMutex 会自动重新被锁定。

下面是一个简单的示例程序,使用 sync.Cond 实现了一个简单的条件等待机制:

 package main
 ​
 import (
     "fmt"
     "sync"
     "time"
 )
 ​
 func main() {
     var mu sync.Mutex
     cond := sync.NewCond(&mu)
     var ready bool
 ​
     // 模拟一个耗时的初始化操作
     go func() {
         time.Sleep(2 * time.Second)
         mu.Lock()
         ready = true
         cond.Signal() // 唤醒等待的 goroutine
         mu.Unlock()
     }()
 ​
     mu.Lock()
     for !ready {
         cond.Wait() // 等待初始化完成信号
     }
     fmt.Println("Initialization completed")
     mu.Unlock()
 }

上面的示例程序中,我们通过 sync.Cond 实现了一种等待初始化完成的机制。在初始化完成前,主 goroutine 会等待条件变量的信号,当子 goroutine 完成初始化后,会通过 Signal 方法发送唤醒信号,使得主 goroutine 继续执行。

2.3 唤醒等待的 goroutine

sync.Cond 提供了两种方式来唤醒等待的 goroutine:Signal 和 Broadcast。

2.3.1 Signal 方法

Signal 方法用于唤醒等待队列中的一个 goroutine,使其继续执行。在调用 Signal 方法之前,必须先获得 Mutex 或 RWMutex 的锁。

 func (c *Cond) Signal()

Signal 方法会选择等待队列中的一个 goroutine 并唤醒它,如果没有等待的 goroutine,那么 Signal 方法不会产生任何效果。

下面是一个示例程序,演示了如何使用 Signal 方法唤醒等待的 goroutine:

 package main
 ​
 import (
     "fmt"
     "sync"
     "time"
 )
 ​
 func main() {
     var mu sync.Mutex
     cond := sync.NewCond(&mu)
     var ready bool
 ​
     // 模拟一个耗时的初始化操作
     go func() {
         time.Sleep(2 * time.Second)
         mu.Lock()
         ready = true
         cond.Signal() // 唤醒等待的 goroutine
         mu.Unlock()
     }()
 ​
     mu.Lock()
     for !ready {
         cond.Wait() // 等待初始化完成信号
     }
     fmt.Println("Initialization completed")
     mu.Unlock()
 }

在上面的示例程序中,我们通过调用 cond.Signal() 方法来唤醒等待的 goroutine。

2.3.2 Broadcast 方法

Broadcast 方法用于唤醒等待队列中的所有 goroutine,使它们继续执行。在调用 Broadcast 方法之前,必须先获得 Mutex 或 RWMutex 的锁。

 func (c *Cond) Broadcast()

Broadcast 方法会唤醒等待队列中的所有 goroutine,如果没有等待的 goroutine,那么 Broadcast 方法不会产生任何效果。

下面是一个示例程序,演示了如何使用 Broadcast 方法唤醒等待的 goroutine:

 package main
 ​
 import (
     "fmt"
     "sync"
     "time"
 )
 ​
 func main() {
     var mu sync.Mutex
     cond := sync.NewCond(&mu)
     var ready bool
 ​
     // 模拟一个耗时的初始化操作
     go func() {
         time.Sleep(2 * time.Second)
         mu.Lock()
         ready = true
         cond.Broadcast() // 唤醒等待的所有 goroutine
         mu.Unlock()
     }()
 ​
     mu.Lock()
     for !ready {
         cond.Wait() // 等待初始化完成信号
     }
     fmt.Println("Initialization completed")
     mu.Unlock()
 }

在上面的示例程序中,我们通过调用 cond.Broadcast() 方法来唤醒等待的 goroutine。

3. sync.Cond 的内部实现原理

sync.Cond 的内部实现依赖于一个等待队列,它维护了等待条件变量的 goroutine 的列表,其中每个 goroutine 都有一个阻塞的状态。当条件变量被发出信号时,等待队列中的一个 goroutine 将被唤醒,并从 Wait 方法中返回,同时将重新获得 Mutex 的锁。

下面是 sync.Cond 内部的等待队列结构体定义:

 type wait struct {
     // 等待队列中的 goroutine
     // goroutine 在 cond.Wait() 中被加入队列,在 cond.Signal() 或 cond.Broadcast() 中被唤醒
     // 由于队列是单向链表,因此需要保存 next 指针指向下一个元素
     // 当 goroutine 被唤醒时,会将 wait.done 设置为 true,并唤醒 wait.cond.L 上阻塞的 goroutine
     // goroutine 从 Wait() 方法中返回时,会将 wait.done 设置为 true
     // wait.done 可以保证 goroutine 不会重复地从 cond.Wait() 方法中返回
     // wait.done 可以保证 goroutine 在从 cond.Wait() 方法中返回时,已经持有了 Mutex 的锁
     // wait.done 可以保证 goroutine 在被唤醒之前不会在 cond.Wait() 方法中被重新加入到队列中
     done bool
     // 下一个等待队列元素的指针
     next *wait
     // 条件变量
     cond *Cond
 }

sync.Cond 使用 wait 结构体维护了一个等待队列,其中每个元素都代表了一个等待 goroutine。

wait 结构体中的 done 字段用于保证 goroutine 不会重复地从 Wait 方法中返回,next 字段用于链接下一个等待元素。

等待队列的头部和尾部分别使用 wait 结构体的指针 first 和 last 维护。

 type Cond struct {
     // Mutex 保护 condition 变量和等待队列
     L Locker
 ​
     // 等待队列的头部和尾部
     first *wait
     last  *wait
 }

sync.Cond 的 Wait 方法实现如下:

 func (c *Cond) Wait() {
     // 将当前 goroutine 加入到等待队列中
     t := new(wait)
     t.cond = c
     c.add(t)
     defer c.remove(t)
 ​
     // 释放锁并进入阻塞状态
     c.L.Unlock()
     for !t.done {
         runtime.Gosched()
     }
     c.L.Lock()
 }

在 Wait 方法中,首先创建一个 wait 结构体 t,并将当前 goroutine 加入到等待队列中,然后释放 Mutex 的锁,并进入阻塞状态。

在等待队列中,goroutine 的状态为阻塞,直到被唤醒并从 Wait 方法中返回。

当等待的条件变量满足时,唤醒等待队列中的 goroutine 的操作由 Signal 和 Broadcast 方法来实现。

Signal 方法会唤醒等待队列中的一个 goroutine,而 Broadcast 方法会唤醒所有等待队列中的 goroutine。

 func (c *Cond) Signal() {
     if c.first != nil {
         c.first.wake(true)
     }
 }
 ​
 func (c *Cond) Broadcast() {
     for c.first != nil {
         c.first.wake(true)
     }
 }

在 Signal 和 Broadcast 方法中,首先判断等待队列是否为空,如果不为空,则唤醒等待队列中的一个或所有 goroutine,并将它们从阻塞状态中解除。 下面是 wait 结构体的 wake 方法实现:

 func (w *wait) wake(done bool) {
     // 标记 done 字段并解除阻塞状态
     w.done = done
     runtime.NotifyListNotify(&w.cond.L.(*Mutex).notify)
 }

在 wake 方法中,首先将 wait.done 设置为 true,然后通过调用 runtime.NotifyListNotify 方法,将等待队列中的 goroutine 从阻塞状态中解除。

这里需要注意的是,在 sync.Cond 的实现中,使用了 Mutex 的 notify 字段来实现 goroutine 的唤醒和阻塞。

当一个 goroutine 调用 Wait 方法时,它会释放 Mutex 的锁,并进入阻塞状态,同时将自己加入到 Mutex 的 notify 队列中。

当一个 goroutine 调用 Signal 或 Broadcast 方法时,它会从 Mutex 的 notify 队列中取出一个或多个 goroutine,并唤醒它们。

这种实现方式与操作系统的线程调度机制类似,可以保证唤醒的 goroutine 在调用 Wait 方法时已经持有了 Mutex 的锁,从而避免了死锁和竞态条件等问题。

这里再补充一下 Mutex 的 notify 字段的定义:

 type Mutex struct {
     state int32
     sema  uint32
     waitm uint32
     notify notifyList
 }

notify 字段是一个 notifyList 类型的对象,它定义如下:

 type notifyList struct {
     wait   uint32 // 等待的 goroutine 的数量
     notify uint32 // 唤醒的 goroutine 的数量
     head   *wait  // 等待队列的头部元素
     tail   *wait  // 等待队列的尾部元素
 }

notifyList 类型的对象维护了一个等待队列和唤醒队列,其中等待队列用于存放阻塞的 goroutine,唤醒队列用于存放将要被唤醒的 goroutine。

notifyList 类型的对象还维护了等待队列和唤醒队列中 goroutine 的数量。

当一个 goroutine 调用 Wait 方法时,它会将自己加入到等待队列中,并且将 Mutex 的 waitm 字段加一。

当一个 goroutine 调用 Signal 或 Broadcast 方法时,它会从等待队列中取出一个或多个 goroutine,并将它们加入到唤醒队列中。

当一个 goroutine 调用 Unlock 方法时,它会判断唤醒队列中是否有 goroutine 需要唤醒,并将 Mutex 的 sema 字段加一,从而使得下一个 goroutine 获得锁。

4. sync.Cond 的使用方法

sync.Cond 的使用方法通常包括以下步骤:

1.定义互斥锁和条件变量。

 var mutex sync.Mutex
 var cond = sync.NewCond(&mutex)

2.在生产者和消费者之间使用互斥锁和条件变量进行同步。

 package main
 ​
 import (
     "fmt"
     "math/rand"
     "sync"
     "time"
 )
 ​
 type Queue struct {
     items []int
     size  int
     lock  sync.Mutex
     cond  *sync.Cond
 }
 ​
 func NewQueue(size int) *Queue {
     q := &Queue{
         items: make([]int, 0, size),
         size:  size,
     }
     q.cond = sync.NewCond(&q.lock)
     return q
 }
 ​
 func (q *Queue) Put(item int) {
     q.lock.Lock()
     defer q.lock.Unlock()
 ​
     for len(q.items) == q.size {
         q.cond.Wait()
     }
 ​
     q.items = append(q.items, item)
     fmt.Printf("put item %d, queue len %d\n", item, len(q.items))
 ​
     q.cond.Signal()
 }
 ​
 func (q *Queue) Get() int {
     q.lock.Lock()
     defer q.lock.Unlock()
 ​
     for len(q.items) == 0 {
         q.cond.Wait()
     }
 ​
     item := q.items[0]
     q.items = q.items[1:]
     fmt.Printf("get item %d, queue len %d\n", item, len(q.items))
 ​
     q.cond.Signal()
     return item
 }
 ​
 func Producer(q *Queue, id int) {
     for {
         item := rand.Intn(100)
         q.Put(item)
         time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
     }
 }
 ​
 func Consumer(q *Queue, id int) {
     for {
         item := q.Get()
         time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
     }
 }
 ​
 func main() {
     q := NewQueue(5)
     for i := 0; i < 3; i++ {
         go Producer(q, i)
     }
     for i := 0; i < 5; i++ {
         go Consumer(q, i)
     }
     time.Sleep(10 * time.Second)
 }

在这个例子中,我们创建了一个 Queue 类型,它包含一个整数数组和一个长度。在 Put 和 Get 方法中,我们使用互斥锁和条件变量进行同步。

在 Producer 和 Consumer 函数中,我们模拟生产者和消费者的行为。生产者会不断地生成随机数,并调用 Put 方法将其放入队列中;消费者会不断地调用 Get 方法从队列中取出数据。

在主函数中,我们创建了多个生产者和消费者 goroutine,它们并发地操作队列。在程序运行过程中,我们可以看到队列的长度会不断地变化,生产者和消费者会交替执行。

5. 总结

sync.Cond 是 Go 语言中非常重要的同步原语之一。它可以帮助我们实现更高级别的同步机制,例如生产者和消费者模型、读写锁等。同时,它也是一个非常复杂的数据结构,需要深入理解其内部实现才能正确地使用它。

在使用 sync.Cond 时,我们需要注意以下几点:

  • 在使用 sync.Cond 前,一定要先创建一个互斥锁。
  • 在调用 Wait 方法前,一定要先获取互斥锁,否则会导致死锁。
  • 在调用 Wait 方法后,当前 goroutine 会被阻塞,直到被唤醒。
  • 在调用 Signal 或 Broadcast 方法后,等待队列中的一个或多个 goroutine 会被唤醒,但不会立即获取互斥锁。因此,在使用 Signal 或 Broadcast 方法时,一定要保证唤醒的 goroutine 不会互相竞争同一个资源。
  • 在调用 Signal 或 Broadcast 方法后,一定要释放互斥锁,否则被唤醒的 goroutine 无法获取到互斥锁,仍然会被阻塞。
  • 在使用 sync.Cond 时,一定要注意竞争条件和数据同步的问题,确保程序的正确性和稳定性。

在本文中,我们介绍了 sync.Cond 的基本用法和内部实现原理,并通过一个实际的生产者和消费者模型的例子,展示了如何使用 sync.Cond 实现高级别的同步机制。

使用 sync.Cond 可以帮助我们实现更高效、更灵活、更安全的并发程序。但同时,也需要我们仔细思考和理解其内部实现,避免出现竞争条件和数据同步的问题,确保程序的正确性和稳定性。

总之,Golang 的 sync.Cond 类型是 Golang 并发编程中非常重要的一个组件,熟练掌握它的使用方法和实现原理,可以有效提升 Golang 并发编程的能力和水平。

以上就是深入学习Golang并发编程必备利器之sync.Cond类型的详细内容,更多关于Golang sync.Cond的资料请关注脚本之家其它相关文章!

相关文章

  • Go语言开发kube-scheduler整体架构深度剖析

    Go语言开发kube-scheduler整体架构深度剖析

    这篇文章主要为大家介绍了Go语言开发kube-scheduler整体架构深度剖析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-04-04
  • Go并发的方法之goroutine模型与调度策略

    Go并发的方法之goroutine模型与调度策略

    在go中,协程co-routine被改为goroutine,一个goroutine只占几kb,因此可以有大量的goroutine存在,另一方面goroutine 的调度器非常灵活,本文给大家介绍下Go并发的方法之goroutine模型与调度策略,感兴趣的朋友一起看看吧
    2021-11-11
  • golang中如何保证精度的方法

    golang中如何保证精度的方法

    本文主要介绍了golang中如何保证精度的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-04-04
  • goland 导入github包报红问题解决

    goland 导入github包报红问题解决

    本文主要介绍了Go项目在GoLand中导入依赖标红问题解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2024-08-08
  • 谈谈Go语言的反射三定律

    谈谈Go语言的反射三定律

    本文中,我们将解释Go语言中反射的运作机制。每个编程语言的反射模型不大相同,很多语言索性就不支持反射(C、C++)。由于本文是介绍Go语言的,所以当我们谈到“反射”时,默认为是Go语言中的反射。
    2016-08-08
  • Go语言包管理模式示例分析

    Go语言包管理模式示例分析

    这篇文章主要为大家介绍了Go语言包管理模式示例分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-05-05
  • 深入讲解Go语言中函数new与make的使用和区别

    深入讲解Go语言中函数new与make的使用和区别

    大家都知道Go语言中的函数new与函数make一直是新手比较容易混淆的东西,看着相似,但其实不同,不过解释两者之间的不同也非常容易,下面这篇文章主要给大家介绍了关于Go语言中函数new与make区别的相关资料,需要的朋友可以参考下。
    2017-10-10
  • 基于Golang编写贪吃蛇游戏

    基于Golang编写贪吃蛇游戏

    这篇文章主要为大家学习介绍了Golang如何基于终端库termbox-go做个功能较简单的贪吃蛇游戏,文中的示例代码讲解详细,具有一定的学习价值
    2023-07-07
  • 使用Go语言实现找出两个大文件中相同的记录

    使用Go语言实现找出两个大文件中相同的记录

    这篇文章主要为大家详细介绍了使用Go语言实现找出两个大文件中相同的记录的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2024-10-10
  • Golang开发命令行之flag包的使用方法

    Golang开发命令行之flag包的使用方法

    这篇文章主要介绍Golang开发命令行及flag包的使用方法,日常命令行操作,相对应的众多命令行工具是提高生产力的必备工具,本文围绕该内容展开话题,需要的朋友可以参考一下
    2021-10-10

最新评论