Go语言sync.Cond使用方法详解

 更新时间:2023年07月18日 11:15:48   作者:码一行  
Go语言标准库中还包含条件变量 sync.Cond,它可以让一组 Goroutine 都在满足特定条件时被唤醒,每一个sync.Cond结构体在初始化时都需要传入一个互斥锁,接下来我们将通过文中例子了解它的使用方法,感兴趣的同学跟着小编一起来看看吧

概述

每一个sync.Cond结构体在初始化时都需要传入一个互斥锁,我们可以通过下面的例子了解它的使用方法:

var status int64
func main(){
    c := sync.NewCond(&sync.mutex{})
    for i := 0; i < 10; i++ {
        go listen(c)
    }
    time.Sleep(1 * time.Second)
    go broadcast(c)
    ch := make(chan os.Signal, 1)
    signal.Notify(ch, os.Interrupt)
    <-ch
}
func broadcast(c *sync.Cond) {
    c.L.Lock()
    atomic.StoreInt64(&status, 1)
    c.Broadcast()
    c.L.Unlock()
}
func listen(c *sync.Cond) {
    c.L.Lock()
    for atomic.LoadInt64(&status) != 1 {
        c.Wait()
    }
    fmt.Println("listen")
    c.L.Unlock()
}

运行结果:

listen
...
listen

上述代码同时运行了 11Goroutine,它们分别做了不同事情:

  • 10Goroutine通过sync.Cond.Wait等待特定条件满足
  • 1Goroutine会调用sync.Cond.Broadcast唤醒所有陷入等待的Goroutine

调用sync.Cond.Broadcast方法后,上述代码会打印出10"listen" 并结束调用。

结构体

sync.Cond的结构体中包含以下 4 个字段:

type Cond struct {
    noCopy   noCopy
    L        Locker
    notify   notifyList
    checker  copyChecker
}
  • noCopy —— 用于保证结构体不会在编译期间复制
  • L —— 用于保护内部的 notify 字段,Locker 接口类型的变量
  • notify —— 一个 Goroutine 的链表,它是实现同步机制的核心结构
  • copyChecker —— 用于禁止运行期间发生的复制
type notifyList struct {
    wait   uint32
    notify uint32
    lock   mutex
    head   *sudog
    tail   *sudog
}

sync.notifyList结构体中,headtail分别指向链表的头和尾,waitnotify分别表示当前正在等待的和已经通知的Goroutine的索引。

接口

sync.Cond对外暴露的sync.Cond.Wait方法会令当前Goroutine陷入休眠状态,它的执行过程分成以下两个步骤:

  • 调用runtime.notifyListAdd将等待计时器加一并解锁
  • 调用runtime.notifyListWait等待其他Goroutine被唤醒并对其加锁
func (c *Cond) Wait () {
    c.checker.check()
    t := runtime_notifyListAdd(&c.notify)  // runtime.notifyListAdd 的链接名
    c.L.Unlock()
    runtime_notifyListWait(&c.notify, t)   //runtime.notifyListWait 的链接名
    c.L.Lock()
}
func notifyListAdd(l *notifyList) uint32 {
    return atomic.Xadd(&l.wait, 1) - 1
}

runtime.notifyListWait 会获取当前Goroutine并将它追加到Goroutine通知链表的末端:

func notifyListWait(l *notifyList, t uint32) {
    s := acquireSudog()
    s.g = getg()
    s.ticket = t
    if l.tail == nil {
        l.head = s
    } else {
        l.tail.next = s
    }
    l.tail = s
    goparkunlock(&l.lock, waitReasonSyncCondWait, traceEvGoBlockCond, 3)
    releaseSudog(s)
}

除了将当前Goroutine追加到链表末端外,我们还会调用runtime.goparkunlock令当前Goroutine陷入休眠。该函数也是在Go语言切换Goroutine时常用的方法,它会直接让出当前处理器的使用权并等待调度器唤醒。

sync.Cond.Signalsync.Cond.Broadcast方法就是用来唤醒陷入休眠的Goroutine的,它们的实现有一些细微差别:

  • sync.Cond.Signal方法会唤醒队列最前面的Goroutine
  • sync.Cond.Broadcast方法会唤醒队列中全部Goroutine
func (c *Cond) Signal() {
    c.checker.check()
    runtime_notifyListNotifyOne(&c.notify)
}
func (c *Cond) Broadcast() {
    c.checker.check()
    runtime_notifyListNotifyAll(&c.notify)
}

runtime.notifyListNotifyOne只会从sync.notifyList链表中找到满足sudog.ticket == l.notify条件的Goroutine,并通过runtime.readyWithTime将其唤醒:

func notifyListNotifyOne(l *notifyList) {
    t := l.notify
    atomic.Store(&l.notify, t + 1)
    for p, s := (*sudog)(nil), l.head; s != nil; p, s = s, s.next {
        if s.tiket == t {
            n := s.next
            if p != nil {
                p.next = n
            } else {
                l.head = n
            }
            if n == nil {
                l.tail = p
            }
            s.next = nil
            readyWithTime(s, 4)
            return
        }
    }
}

runtime.notifyListNotifyAll会依次通过runtime.readyWithTime唤醒链表中的Goroutine

func notifyListNotifyAll(l *notifyList) {
    s := l.head
    l.head = nil
    l.tail = nil
    atomic.Store(&l.notify, atomic.Load(&l.wait))
    for s != nil {
        next := s.next
        s.next = nil
        readyWithTime(s, 4)
        s = next
    }
}

Goroutine的唤醒顺序也是按照加入队列的先后顺序,先加入的会先被唤醒,而后加入的Goroutine可能需要等待调度器的调度。

一般情况下,我们会先调用sync.Cond.Wait陷入休眠等待满足期望条件,当满足期望条件时,就可以选用sync.Cond.Signal或者sync.Cond.Broadcast唤醒一个或者全部Goroutine

小结

sync.Cond不是常用的同步机制,但是在条件长时间无法满足时,与使用for {}进行忙碌等待相比,sync.Cond能够让出处理器的使用权,提高CPU的利用率。

使用时需要注意以下问题:

  • sync.Cond.Wait在调用之前一定要先获取互斥锁,否则会触发程序崩溃
  • sync.Cond.Signal唤醒的Goroutine都是队列最前面、等待最久的Goroutine
  • sync.Cond.Broadcast会按照一定顺序广播通知等待的全部Goroutine

到此这篇关于Go语言sync.Cond使用方法详解的文章就介绍到这了,更多相关Go语言sync.Cond内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • go module化 import 调用本地模块 tidy的方法

    go module化 import 调用本地模块 tidy的方法

    这篇文章主要介绍了go module化 import 调用本地模块 tidy的相关知识,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-09-09
  • Go语言使用组合的方式实现多继承的方法

    Go语言使用组合的方式实现多继承的方法

    这篇文章主要介绍了Go语言使用组合的方式实现多继承的方法,实例分析了多继承的原理与使用组合方式来实现多继承的技巧,需要的朋友可以参考下
    2015-02-02
  • go语言实现一个简单的http客户端抓取远程url的方法

    go语言实现一个简单的http客户端抓取远程url的方法

    这篇文章主要介绍了go语言实现一个简单的http客户端抓取远程url的方法,实例分析了Go语言http操作技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-03-03
  • go语言中的二维切片赋值

    go语言中的二维切片赋值

    这篇文章主要介绍了go语言中的二维切片赋值操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • go-micro微服务JWT跨域认证问题

    go-micro微服务JWT跨域认证问题

    JWT 以 JSON 对象的形式安全传递信息。因为存在数字签名,因此所传递的信息是安全的,这篇文章主要介绍了go-micro微服务JWT跨域认证,需要的朋友可以参考下
    2023-01-01
  • Golang 使用Map实现去重与set的功能操作

    Golang 使用Map实现去重与set的功能操作

    这篇文章主要介绍了Golang 使用 Map 实现去重与 set 的功能操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • Goland的设置与配置全过程

    Goland的设置与配置全过程

    这篇文章主要介绍了Goland的设置与配置全过程,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-02-02
  • Go语言实现逐行读取和写入文件详解

    Go语言实现逐行读取和写入文件详解

    这篇文章主要介绍了如何使用go语言实现从输入文件中读取每行数据,然后将每行字段组合成SQL插入脚本,然后逐行写入另外一个空白文件中,有需要的可以参考下
    2024-01-01
  • Golang 日志处理和正则处理的操作方法

    Golang 日志处理和正则处理的操作方法

    这篇文章主要介绍了Golang 日志处理和正则处理的操作方法,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2025-06-06
  • go语言中的defer关键字

    go语言中的defer关键字

    这篇文章介绍了go语言中的defer关键字,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-07-07

最新评论