乐趣区

关于golang:Go语言定时器的实现

微信公众号:LinuGo,欢送关注

咱们都晓得,Time.sleep(d duration) 办法会阻塞一个协程的执行直到 d 工夫完结。

用法很简略,但外部实现却是大有文章,每个 go 版本的 timer 的实现都有所不同,本文基于 go1.14,接下来别离从宏观和围观介绍一遍次要调度实现过程。


图文演示

上面介绍一种最简略的场景:

首先存在多个 goroutine,GT 为有 time.Sleep 休眠的 g,当 GT 被调度到 m 上执行时,场景如下图。

此时执行到了 time.Sleep 代码,GT 会与 m 解绑,同时将该 GT 的 sleep 工夫等信息记录到 P 的 timers 字段上,此时 GT 处于 Gwaiting 状态,不在运行队列上,调度器会调度一个新的 G2 到 M 上执行。(在每次调度过程中,会查看 P 外面记录的定时器,看看有没有要执行的。)

G2 执行完了,当要进行下一轮调度时,调度器查看本人记录的定时器时发现,GT 到工夫了,是时候执行了。因为工作紧急,GT 就会被强行插入到 P 的运行队列的对头,保障能马上被执行到。

接下来就会间接调度到 GT 执行了,睡眠完结。接下来追随这个简略场景看一下源码实现。

阶段一、进入睡眠

首先调用 time.Sleep(1) 会通过编译器辨认 //go:linkname 链接进入到 runtime.timeSleep(n int) 办法。

//go:linkname timeSleep time.Sleep
func timeSleep(ns int64) { 
   if ns <= 0 { // 判断入参是否失常
      return
   }   
   gp := getg() // 获取以后的 goroutine
   t := gp.timer // 如果不存在 timer,new 一个
   if t == nil {t = new(timer)
      gp.timer = t
   }
   t.f = goroutineReady  // 前面唤醒时候会用到,批改 goroutine 状态为 goready
   t.arg = gp
   t.nextwhen = nanotime() + ns  // 记录上唤醒工夫
   gopark(resetForSleep, unsafe.Pointer(t), waitReasonSleep, traceEvGoSleep, 1)  // 调用 gopark 挂起 goroutine
}

resetForSleep 作为一个函数入参,他的调用栈顺次为 resettimer(t, t.nextwhen) -> modtimer(t, when, t.period, t.f, t.arg, t.seq)(后文会讲到),在前面的 modtimer 外面会将 timer 定时器退出到以后 goroutine 所在的 p 中,定时器在 p 中的构造为一个四叉堆,最近的工夫的放在最堆顶上,对于这个数据结构没有做深入研究。

接下来看一下 gopark 中重要的局部。

func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) { 
   ...... 省略了大部分代码     
   mp.waitlock = lock  // 因为 runningG 和 p 没有连贯,将 timer 赋值到以后 m 上,前面会给到 p
   mp.waitunlockf = unlockf  // 将函数付给 m
   ......
   mcall(park_m) // 将以后的 g 停放
}

看一下 gopark 外面的 mcall 外面的回调函数 park_m 中的局部。

func park_m(gp *g) {_g_ := getg()  // 获取以后 goroutine
   ......

   casgstatus(gp, _Grunning, _Gwaiting)  // 将 goroutine 状态设为 waiting
   dropg()

   if fn := _g_.m.waitunlockf; fn != nil { // 获取到 mresetForSleep 函数
      ok := fn(gp, _g_.m.waitlock) // 返回值是 true
      _g_.m.waitunlockf = nil  // 清空该 m 的函数空间
      _g_.m.waitlock = nil //...
   ......      }
   schedule()  // 触发新的调速循环,可执行队列中获取 g 到 m 上进行调度}

看一下 resetForSleep 回调函数,外面顺次调用了 resettimer(t, t.nextwhen) -> modtimer(t, when, t.period, t.f, t.arg, t.seq),resettimer 函数没有什么重要信息,只负责返回一个 true,看一下 modtimer 函数。

func modtimer(t *timer, when, period int64, f func(interface{}, uintptr), arg interface{}, seq uintptr) {  
   ...... 
loop:  
   for {switch status = atomic.Load(&t.status); status {
      ......
      case timerNoStatus, timerRemoved:   // 因为刚创立,所以 timer 为默认值 0,对应 timerNoStatus
         mp = acquirem()
         if atomic.Cas(&t.status, status, timerModifying) {
            wasRemoved = true   // 设置标记位为 true
            break loop
         }
         releasem(mp)
              badTimer()}
   }

   t.period = period
   t.f = f // 上文传过去的 goroutineReady 函数,用于将 g 转变为 runnable 状态
   t.arg = arg  // 上文的 g 实例
   t.seq = seq 

   if wasRemoved { // 会执行到此处
      t.when = when 
      pp := getg().m.p.ptr() // 获取以后的 p 的指针
      lock(&pp.timersLock) // 加锁,为了并发平安,因为 timer 能够去其余的 p 偷取
      doaddtimer(pp, t) // 增加定时器到以后的 p
      unlock(&pp.timersLock) // 解锁
      if !atomic.Cas(&t.status, timerModifying, timerWaiting) { // 转变到 timerWaiting
         badTimer()} 
      ......
}

当触发完 gopark 办法,该 goroutine 脱离以后的 m 挂起,进入 gwaiting 状态,不在任何运行队列上。对应上图 2。


阶段二、复原执行

执行的复原会在 shedule() 或者 findRunnable() 函数上,外部 checkTimers(pp, 0) 办法,该办法外部会判断 p 中 timers 堆顶的定时器,如果工夫到了的话 (以后工夫大于计算的工夫),调用 runtime.runOneTimer,该办法外面会一系列调用到 goready 办法开释阻塞的 goroutine,并将该 goroutine 放到运行队列的第一个。

接下来看一下 checkTimers 函数:

func checkTimers(pp *p, now int64) (rnow, pollUntil int64, ran bool) { 

   ...... 省略掉调整计时器工夫的一些步骤
   lock(&pp.timersLock) // 加锁
   adjusttimers(pp) // 调整计时器的工夫
   rnow = now
   if len(pp.timers) > 0 {
      if rnow == 0 {rnow = nanotime()
      }
      for len(pp.timers) > 0 {if tw := runtimer(pp, rnow); tw != 0 { // 进入 runtimer 办法,携带零碎工夫参数与处理器
            if tw > 0 {pollUntil = tw}
            break
         }
         ran = true
      }
   }
......
}

进入 runtimer 办法,会查看 p 外面的堆顶的定时器,查看是否须要执行。

func runtimer(pp *p, now int64) int64 {  
   for {t := pp.timers[0] // 遍历堆顶的定时器
     .......
      switch s := atomic.Load(&t.status); s {
      case timerWaiting:  // 通过 time.Sleep 的定时器会是 waiting 状态
         if t.when > now {  // 判断是否超过工夫
             // Not ready to run.
            return t.when
         }

         if !atomic.Cas(&t.status, s, timerRunning) {  // 批改计时器状态
            continue
         }
         runOneTimer(pp, t, now) // 运行该计时器函数
         return 0        ........

接下来调用 runOneTimer 函数解决。

func runOneTimer(pp *p, t *timer, now int64) {  
........

   f := t.f //goready 函数
   arg := t.arg  // 就是之前传入的 goroutine
   seq := t.seq  // 默认值 0

   if t.period > 0 {.........  // 因为 period 为默认值 0,会走 else 外面} else {dodeltimer0(pp)  // 删除该计时器在 p 中,该 timer 在 0 坐标位
      if !atomic.Cas(&t.status, timerRunning, timerNoStatus) {  // 设置为 nostatus
         badTimer()}
   }.......
   unlock(&pp.timersLock)

   f(arg, seq) // 执行 goroutineReady 办法,唤起期待的 goroutine
   .........
}

看一下下面的 f(arg,seq) 即 goroutineReady 办法的实现,该函数的实现就是间接调用了 goready 办法唤起 goroutine,对应上图 3:

func goroutineReady(arg interface{}, seq uintptr) {goready(arg.(*g), 0) // 该处传入的第二个参数代表调度到运行队列的地位,该处设置为 0,阐明间接调度到运行队列行将要执行的地位,期待被执行。}

另外,系统监控 sysmon 函数也能够触发定时器的调用,该函数是一个循环查看零碎中是否领有应该被运行然而还在期待的定时器,并调度他们运行。

对于 time.NewTimer 函数等,实现办法也是大抵类似,只是回调函数变成了 sendTime 函数,该函数不会阻塞。调用该函数后,睡眠的 goroutine 会从 channel 中开释并退出运行队列,有趣味能够本人钻研一下。

以上就是整个 time.sleep 的调度过程,你能够依据我总结的对照源码一步一步看,必定会加深印象,深刻了解。

参考文章

【1】《Go 计时器》https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-timer/

【2】《Golang 定时器底层实现分析》https://www.cyhone.com/articles/analysis-of-golang-timer/

退出移动版