为了账号安全,请及时绑定邮箱和手机立即绑定

睡眠和选择的行为

睡眠和选择的行为

Go
慕丝7291255 2021-11-01 16:21:17
我试图更多地了解在 Go 中各种阻塞/等待类型的操作期间表面下发生的事情。以下面的例子为例:otherChan = make(chan int)t = time.NewTicker(time.Second)for {    doThings()    // OPTION A: Sleep    time.Sleep(time.Second)    // OPTION B: Blocking ticker    <- t.C    // OPTION C: Select multiple    select {        case <- otherChan:        case <- t.C:    }}从底层来看(系统调用、cpu 调度),这些等待时的区别是什么?我的理解是time.Sleep让 CPU 可以自由执行其他任务,直到指定的时间过去。阻塞自动收报机<- t.C也做同样的事情吗?处理器是否在轮询通道或是否涉及中断?选择中有多个频道会改变什么吗?换句话说,假设otherChan从来没有放入任何东西,这三个选项是否会以相同的方式执行,或者一个比其他的资源密集度更低?
查看完整描述

1 回答

?
慕桂英4014372

TA贡献1871条经验 获得超13个赞

这是一个非常有趣的问题,所以我cd进入了我的 Go 源代码开始寻找。


时间.睡眠

time.Sleep 定义如下:


// src/time/sleep.go


// Sleep pauses the current goroutine for at least the duration d.

// A negative or zero duration causes Sleep to return immediately.

func Sleep(d Duration)

没有正文,没有特定于操作系统的定义time_unix.go!?!稍微搜索一下,答案是因为time.Sleep实际上是在运行时中定义的:


// src/runtime/time.go


// timeSleep puts the current goroutine to sleep for at least ns nanoseconds.

//go:linkname timeSleep time.Sleep

func timeSleep(ns int64) {

  // ...

}

回想起来,这很有意义,因为它必须与 goroutine 调度程序进行交互。它最终调用goparkunlock,它“将 goroutine 置于等待状态”。time.Sleep创建一个runtime.timer带有回调函数的回调函数,该函数在计时器到期时调用 - 该回调函数通过调用goready. 有关runtime.timer.


时间.NewTicker

time.NewTicker创建一个*Ticker(并且time.Tick是一个辅助函数,它做同样的事情但直接返回*Ticker.C,股票代码的接收通道,而不是*Ticker,所以你可以用它来编写你的代码)在运行时有类似的钩子:股票代码是一个结构持有一个runtimeTimer和一个通道,在其上发出信号。


runtimeTimer在time包中定义,但它必须与timerin保持同步src/runtime/time.go,因此它实际上是一个runtime.timer. 还记得在time.Sleep,定时器有一个回调函数来唤醒休眠的 goroutine 吗?在 的情况下*Ticker,定时器的回调函数在股票代码的通道上发送当前时间。


然后,真正的等待/调度发生在从通道接收时,这与select语句基本相同,除非otherChan在滴答之前发送一些东西,所以让我们看看阻塞接收时会发生什么。


<- 陈

通道src/runtime/chan.go由hchan结构体在,中实现(现在在 Go 中!)。通道操作具有匹配功能,接收是通过chanrecv以下方式实现的:


// chanrecv receives on channel c and writes the received data to ep.

// ep may be nil, in which case received data is ignored.

// If block == false and no elements are available, returns (false, false).

// Otherwise, if c is closed, zeros *ep and returns (true, false).

// Otherwise, fills in *ep with an element and returns (true, true).

func chanrecv(t *chantype, c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {

  // ...

}

这部分有很多不同的情况,但在您的示例中,它是来自异步通道的阻塞接收(time.NewTicker创建一个缓冲区为 1 的通道),但无论如何它最终会调用... goparkunlock,再次允许其他 goroutines在这个被卡住等待时继续。


所以...

在所有情况下,goroutine 最终都会被停住(这并不令人震惊 - 它无法取得进展,因此如果有任何可用的 goroutine,它必须保留其线程可用于不同的 goroutine)。看一眼代码似乎表明该通道的开销比直接time.Sleep. 但是,它允许更强大的模式,例如您示例中的最后一个:goroutine 可以被另一个通道唤醒,以先到者为准。


要回答关于轮询的其他问题,定时器由一个 goroutine 管理,该协程在队列中的下一个定时器之前一直处于休眠状态,因此它仅在知道必须触发定时器时才工作。当下一个计时器到期时,它会唤醒调用的 goroutine time.Sleep(或在股票代码的通道上发送值,它会执行回调函数所做的任何事情)。


频道中没有轮询,当在频道上进行发送时,接收被解锁,在chansendchan.go 文件中:


// wake up a waiting receiver

sg := c.recvq.dequeue()

if sg != nil {

    recvg := sg.g

    unlock(&c.lock)

    if sg.releasetime != 0 {

        sg.releasetime = cputicks()

    }

    goready(recvg, 3)

} else {

    unlock(&c.lock)

}

这是对 Go 源代码的一个有趣的探索,非常有趣的问题!希望我至少回答了一部分!


查看完整回答
反对 回复 2021-11-01
  • 1 回答
  • 0 关注
  • 166 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信