我试图更多地了解在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
从未有过任何内容,这三个选项会以相同的方式执行,还是会比其他选项的资源密集程度更低?
答案 0 :(得分:13)
这是一个非常有趣的问题,所以我在我的Go源代码中cd
开始寻找。
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
创建了一个*Ticker
(而time.Tick
是一个帮助函数,可以执行相同的操作但直接返回*Ticker.C
,即代码的接收通道,而不是*Ticker
的{{1}},所以你可以用它编写你的代码)在运行时有类似的钩子:自动收报机是一个包含runtimeTimer
的结构和一个用来表示滴答信号的通道
runtimeTimer
在time
包中定义,但必须与timer
中的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继续进行,而这个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的源代码,非常有趣的问题!希望我至少回答了部分内容!