Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Go语言sync.Cond

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

作者:码一行

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,它们分别做了不同事情:

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

结构体

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

type Cond struct {
    noCopy   noCopy
    L        Locker
    notify   notifyList
    checker  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陷入休眠状态,它的执行过程分成以下两个步骤:

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的,它们的实现有一些细微差别:

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的利用率。

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

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

您可能感兴趣的文章:
阅读全文