睡眠的行为和选择在去

时间:2015-08-21 18:48:33

标签: go resources blocking

我试图更多地了解在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 个答案:

答案 0 :(得分:13)

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

time.Sleep

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,其中&#34;将goroutine置于等待状态&#34;。 time.Sleep创建一个runtime.timer,其中包含一个在计时器到期时调用的回调函数 - 该回调函数通过调用goready唤醒goroutine。有关runtime.timer

的更多详细信息,请参阅下一节

time.NewTicker

time.NewTicker创建了一个*Ticker(而time.Tick是一个帮助函数,可以执行相同的操作但直接返回*Ticker.C,即代码的接收通道,而不是*Ticker的{​​{1}},所以你可以用它编写你的代码)在运行时有类似的钩子:自动收报机是一个包含runtimeTimer的结构和一个用来表示滴答信号的通道

runtimeTimertime包中定义,但必须与timer中的src/runtime/time.go保持同步,因此它实际上是runtime.timer。请记住,在time.Sleep中,计时器有一个回调函数来唤醒睡眠goroutine?在*Ticker的情况下,计时器的回调函数会在自动收报机的频道上发送当前时间。

然后,真正的等待/调度发生在来自频道的接收上,这与select语句基本相同,除非otherChan在节拍之前发送内容,所以让我们看一下在阻塞接收上会发生什么。

&lt; - chan

通过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继续进行,而这个goroutines正在等待。

所以...

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

要回答您的其他问题,关于轮询,计时器由goroutine管理,该goroutine会一直睡到队列中的下一个计时器,因此只有在知道必须触发计时器时它才会工作。当下一个计时器到期时,它会唤醒调用time.Sleep的goroutine(或者在自动收录器的通道上发送值,它会执行回调函数所做的任何操作)。

在频道中没有轮询,当在频道上发送时,在chan.go文件的chansend中解锁接收:

// 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的源代码,非常有趣的问题!希望我至少回答了部分内容!