Behavior of sleep and select in go

huangapple go评论79阅读模式
英文:

Behavior of sleep and select in go

问题

我正在尝试更深入地了解Go语言中各种阻塞/等待操作在底层发生了什么。以以下示例为例:

otherChan = make(chan int)
t = time.NewTicker(time.Second)
for {
    doThings()

    // 选项A:休眠
    time.Sleep(time.Second)

    // 选项B:阻塞的定时器
    <- t.C
    
    // 选项C:多路选择
    select {
        case <- otherChan:
        case <- t.C:
    }
}

从底层视角(系统调用、CPU调度)来看,这些等待操作有什么区别?

我理解的是,time.Sleep 在指定的时间内将CPU空闲出来执行其他任务。阻塞的定时器 <- t.C 是否也是如此?处理器是否在轮询通道,还是涉及中断?在选择语句中有多个通道是否会改变情况?

换句话说,假设 otherChan 从未放入任何内容,这三个选项是否以相同的方式执行,或者其中一个比其他选项更节省资源?

英文:

I'm trying to understand a bit more about what happens under the surface during various blocking/waiting types of operations in Go. Take the following example:

otherChan = make(chan int)
t = time.NewTicker(time.Second)
for {
    doThings()

    // OPTION A: Sleep
    time.Sleep(time.Second)

    // OPTION B: Blocking ticker
    &lt;- t.C
    
    // OPTION C: Select multiple
    select {
        case &lt;- otherChan:
        case &lt;- t.C:
    }
}

From a low level view (system calls, cpu scheduling) what is the difference between these while waiting?

My understanding is that time.Sleep leaves the CPU free to perform other tasks until the specified time has elapsed. Does the blocking ticker &lt;- t.C do the same? Is the processor polling the channel or is there an interrupt involved? Does having multiple channels in a select change anything?

In other words, assuming that otherChan never had anything put into it, would these three options execute in an identical way, or would one be less resource intensive than the others?

答案1

得分: 37

这是一个非常有趣的问题,所以我使用cd命令进入我的Go源代码目录开始查找。

time.Sleep

time.Sleep的定义如下:

// src/time/sleep.go

// Sleep将当前goroutine暂停至少持续时间d。
// 负值或零持续时间会导致Sleep立即返回。
func Sleep(d Duration)

没有函数体,在特定于操作系统的time_unix.go中也没有定义!?稍微搜索一下,答案是因为time.Sleep实际上是在runtime中定义的:

// src/runtime/time.go

// timeSleep使当前goroutine至少休眠ns纳秒。
//go:linkname timeSleep time.Sleep
func timeSleep(ns int64) {
  // ...
}

回过头来看,这是有道理的,因为它必须与goroutine调度器进行交互。它最终调用goparkunlock,该函数将"将goroutine置于等待状态"。time.Sleep创建一个带有回调函数的runtime.timer,当计时器到期时,回调函数会唤醒goroutine,调用goready。有关runtime.timer的更多详细信息,请参见下一节。

time.NewTicker

time.NewTicker创建一个*Tickertime.Tick是一个辅助函数,它执行相同的操作,但直接返回*Ticker.C,即ticker的接收通道,而不是*Ticker,所以你可以使用它来编写代码)也与runtime有类似的钩子:ticker是一个结构体,它包含一个runtimeTimer和一个用于发送tick信号的通道。

runtimeTimertime包中定义,但必须与src/runtime/time.go中的timer保持同步,因此它实际上是一个runtime.timer。记住,在time.Sleep中,计时器有一个回调函数来唤醒正在睡眠的goroutine吗?对于*Ticker,计时器的回调函数会在ticker的通道上发送当前时间。

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

<- chan

通道(现在在Go中实现!)在src/runtime/chan.go中由hchan结构体实现。通道操作有相应的函数,接收操作由chanrecv实现:

// chanrecv从通道c接收数据并将接收到的数据写入ep。
// ep可以为nil,此时接收到的数据将被忽略。
// 如果block == false且没有可用元素,则返回(false,false)。
// 否则,如果c已关闭,则将*ep置为零并返回(true,false)。
// 否则,将*ep填充为一个元素并返回(true,true)。
func chanrecv(t *chantype, c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
  // ...
}

这部分有很多不同的情况,但在你的示例中,它是从异步通道进行阻塞接收的(time.NewTicker创建了一个带有缓冲区为1的通道),但无论如何,它最终都会调用... goparkunlock,再次允许其他goroutine继续执行,而该goroutine则被阻塞等待。

所以...

在所有情况下,goroutine最终都会被停放(这并不令人震惊-它无法取得进展,因此必须将其线程留给其他可用的goroutine)。快速浏览代码似乎表明,通道的开销比直接使用time.Sleep要大一些。然而,它允许更强大的模式,例如你示例中的最后一个模式:goroutine可以通过另一个通道唤醒,以先到者为准。

至于你的其他问题,关于轮询,计时器由一个休眠直到其队列中的下一个计时器的goroutine管理,因此它仅在知道计时器必须触发时才工作。当下一个计时器过期时,它会唤醒调用time.Sleep的goroutine(或者将值发送到ticker的通道上,它执行回调函数的操作)。

通道中没有轮询,当在通道上进行发送时,接收操作会解锁,这在chan.go文件的chansend中实现:

// 唤醒等待的接收方
sg := c.recvq.dequeue()
if sg != nil {
	recvg := sg.g
	unlock(&amp;c.lock)
	if sg.releasetime != 0 {
		sg.releasetime = cputicks()
	}
	goready(recvg, 3)
} else {
	unlock(&amp;c.lock)
}

这是一个有趣的深入Go源代码的过程,非常有趣的问题!希望我至少回答了部分问题!

英文:

That's a very interesting question, so I did cd into my Go source to start looking.

time.Sleep

time.Sleep is defined like this:

// 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)

No body, no definition in an OS-specific time_unix.go!?! A little search and the answer is because time.Sleep is actually defined in the runtime:

// 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) {
  // ...
}

Which in retrospect makes a lot of sense, as it has to interact with the goroutine scheduler. It ends up calling goparkunlock, which "puts the goroutine into a waiting state". time.Sleep creates a runtime.timer with a callback function that is called when the timer expires - that callback function wakes up the goroutine by calling goready. See next section for more details on the runtime.timer.

time.NewTicker

time.NewTicker creates a *Ticker (and time.Tick is a helper function that does the same thing but directly returns *Ticker.C, the ticker's receive channel, instead of *Ticker, so you could've written your code with it instead) has similar hooks into the runtime: a ticker is a struct that holds a runtimeTimer and a channel on which to signal the ticks.

runtimeTimer is defined in the time package but it must be kept in sync with timer in src/runtime/time.go, so it is effectively a runtime.timer. Remember that in time.Sleep, the timer had a callback function to wake up the sleeping goroutine? In the case of *Ticker, the timer's callback function sends the current time on the ticker's channel.

Then, the real waiting/scheduling happens on the receive from the channel, which is essentially the same as the select statement unless otherChan sends something before the tick, so let's look at what happens on a blocking receive.

<- chan

Channels are implemented (now in Go!) in src/runtime/chan.go, by the hchan struct. Channel operations have matching functions, and a receive is implemented by 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) {
  // ...
}

This part has a lot of different cases, but in your example, it is a blocking receive from an asynchronous channel (time.NewTicker creates a channel with a buffer of 1), but anyway it ends up calling... goparkunlock, again to allow other goroutines to proceed while this one is stuck waiting.

So...

In all cases, the goroutine ends up being parked (which is not really shocking - it can't make progress, so it has to leave its thread available for a different goroutine if there's any available). A glance at the code seems to suggest that the channel has a bit more overhead than a straight-up time.Sleep. However, it allows far more powerful patterns, such as the last one in your example: the goroutine can be woken up by another channel, whichever comes first.

To answer your other questions, regarding polling, the timers are managed by a goroutine that sleeps until the next timer in its queue, so it's working only when it knows a timer has to be triggered. When the next timer has expired, it wakes up the goroutine that called time.Sleep (or sends the value on the ticker's channel, it does whatever the callback function does).

There's no polling in channels, the receive is unlocked when a send is made on the channel, in chansend of the chan.go file:

// wake up a waiting receiver
sg := c.recvq.dequeue()
if sg != nil {
	recvg := sg.g
	unlock(&amp;c.lock)
	if sg.releasetime != 0 {
		sg.releasetime = cputicks()
	}
	goready(recvg, 3)
} else {
	unlock(&amp;c.lock)
}

That was an interesting dive into Go's source code, very interesting question! Hope I answered at least part of it!

huangapple
  • 本文由 发表于 2015年8月22日 02:48:33
  • 转载请务必保留本文链接:https://go.coder-hub.com/32147421.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定