微信公众号: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/