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内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • golang内存对齐详解

    golang内存对齐详解

    在golang中,每一种数据类型都有其对应的数据类型大小,也就是占用了多少内存空间,我们可以通过unsafe.Sizeof函数,来确定一个变量占用的内存字节数,本文将详细给大家介绍golang内存对齐,需要的朋友可以参考下
    2023-10-10
  • Golang实现对map的并发读写的方法示例

    Golang实现对map的并发读写的方法示例

    这篇文章主要介绍了Golang实现对map的并发读写的方法示例,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-03-03
  • go项目打包部署的完整步骤

    go项目打包部署的完整步骤

    之前断断续续的接触到项目部署,一直没有详细的了解部署,于是最近就好好的专研一下项目的部署,下面这篇文章主要给大家介绍了关于go项目打包部署的相关资料,需要的朋友可以参考下
    2022-09-09
  • GO项目部署Linux服务器的实现示例

    GO项目部署Linux服务器的实现示例

    本文主要介绍了GO项目部署Linux服务器的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2024-06-06
  • 从源码深入理解golang RWMutex读写锁操作

    从源码深入理解golang RWMutex读写锁操作

    这篇文章主要介绍了从源码深入理解golang RWMutex读写锁操作,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-05-05
  • golang框架gin的日志处理和zap lumberjack日志使用方式

    golang框架gin的日志处理和zap lumberjack日志使用方式

    这篇文章主要介绍了golang框架gin的日志处理和zap lumberjack日志使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-01-01
  • Go语言题解LeetCode268丢失的数字示例详解

    Go语言题解LeetCode268丢失的数字示例详解

    这篇文章主要为大家介绍了Go语言题解LeetCode268丢失的数字示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • Go语言区别于其他语言的特性

    Go语言区别于其他语言的特性

    在本文中,今天这篇文章将给大家介绍一下 Go 与其他语言不同的 9 个特性,需要的朋友可以参考下面文章的具体内容
    2021-10-10
  • go中for range的坑以及解决方案

    go中for range的坑以及解决方案

    相信小伙伴都遇到过以下的循环变量的问题,本文主要介绍了go中for range的坑以及解决方案,具有一定的参考价值,感兴趣的可以了解一下
    2024-01-01
  • go benchmark 基准测试详解

    go benchmark 基准测试详解

    这篇文章主要介绍了go benchmark 基准测试详解,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-03-03

最新评论