go-Sleep

引言

time.Sleep(1*time.Second) 这个方法在某些场景很有用,在使用它的时候不由想到。它内部实现原理是什么?如果只是想让出 cpu 时间是不是有更好的方法?执行sleep 后最短回来的时间是多少?

探索分析

首先,我们打开 time.Sleep 源码,出乎意料,一个空的方法,瞬间我迷茫了,难道 go 还有这种使用姿势,定义一个空的方法,然后延迟实现?找了半天不得其法,查完资料才发现在 runtime/time.go 里面有个 timeSleep 方法,有完整的实现。 看注释 //go:linkname timeSleep time.Sleep ,这是一种全新的体验啊,赶紧学习了一下使用姿势。

1
2
3
4
5
6
7
`//go:` 是编译指令,编译器接受注释形式的指令。
实测下来挺坑的,要在具体实现地方引入_ "unsafe” 且在方法添加注释`//go:linkname tSleep swcontent/test/go/sleep/outer.TSleep` ,在声明方法包引入具体实现的包名 import _ "swcontent/test/go/sleep/interval” ,在编译的时候还要用参数忽略错误,简单做法是在声明方法包添加 i.s 空文件。实际使用的价值没那么大,主要知道这个可以辅助我们阅读 go 源码。
还有其他的比如
`//go:nosplit` 跳过栈溢出检查,stack 一开始是2k,会动态增长。
`//go:noinline` 禁止内联函数。
`//go:noescape` 禁止逃逸,即该函数用过就销毁,不会逃逸到堆上。
`//go:norace` 跳过竞争检测,`go run -race main.go` 可以检测

time.Sleep 调用流程是 timeSleep→gopark→resetForSleep→resettimer→modtimer→wakeNetPoller。timeSleep 会创建 timer,并把 goroutineReady 设为其方法。gopark 是很关键的方法,里面主要做了几件事:

  1. 获取当前 g 的 m,设置 waitlock 相关参数。
  2. 调用 mcall 来切换协程,会保存当前 g 的 PC/SP ,在 m->g0 堆栈环境里执行 park_m 方法来切换当前 g 的状态并解绑 m,运行 waitlock 的参数即会调用 resetForSleep (里面会将 timer 添加到定时器中)然后清除 ,然后由 schedule 方法来决定下一个 g 调度。

time.Sleep 曾经有个 bug,在 go 1.4 引入抢占调度后,如果在 timer.status 变更成 timerModifying 后,被调度切换走,那么在 gopreempt_m→goschedImpl→schedule→checkTimers->runtimer 里面会无限等待下去。这块在之后加了防止抢占来修复,从防止抢占方法看实际上是个类似读锁的概念。

结论

  1. go 源码很多切换协程和涉及系统调用的方法都是用汇编写的。
  2. go time.Sleep 原理简单来说就是创建 timer 然后让出线程,最后靠定时系统协程来唤醒。
  3. 如果 timeSleep 很短,理论上在 gopark 时就已经过去了,那么它会执行 wakeup。
  4. 可以用 runtime.Gosched() 来让出 cpu。