深入解析go Timer 和Ticker实现原理

1 timer

timer 简单来说就是1 个定时器,代表多少秒后执行,当创建1个timer,1秒钟过后,我们就能从timer.C 获取那个时刻的时间,因为系统在那个时刻将当前时间写入到timer.C 了,这时候我们就可以做自己的想做的事了。

package main
​
import (
    "fmt"
    "time"
)
​
func main()  {
    timer:=time.NewTimer(1*time.Second)
    defer timer.Stop()
    msg:=<-timer.C
    fmt.Println("1秒后打印",msg)
}
//结果:1秒后打印 2021-11-02 22:43:42.2260892 +0800 CST m=+1.016042901

timer 结构

​
type Timer struct {
    C <-chan Time //存储时间的管道
    r runtimeTimer //底层存储timer 的堆实现
}
  • timer 代表单一的事件
  • 当timer 过期后,当前时间将会被发送到C
  • timer 只能被NewTimer 或者 AfterFunc两个函数创建

runtimeTimer结构

type runtimeTimer struct {
    pp       uintptr
    when     int64 //什么时候触发timer
    period   int64 //如果是周期性任务,执行周期性任务的时间间隔
    f        func(interface{}, uintptr) // NOTE: must not be closure//到时候执行的回调函数
    arg      interface{} //执行任务的参数
    seq      uintptr//回调函数的参数,该参数仅在 netpoll 的应用场景下使用。
    nextwhen int64//如果是周期性任务,下次执行任务时间
    status   uint32//timer 的状态
}

p 上存储timer 的结构

下面只展示跟timer 有关的字段

//Go\src\runtime\runtime2.go +604
type p struct {
    .....
   //堆顶元素什么时候执行
    timer0When uint64
    
    //如果有timer 修改为更早执行时间了,将会将执行时间更新到当更早时间
    timerModifiedEarliest uint64
    
    //操作timer 的互斥锁
    timersLock mutex
​
    //该p 上的所有timer,必须加锁去操作这个字段,因为不同的p 操作这个字段会有竞争关系
    timers []*timer
​
    //p 堆上所有的timer
    numTimers uint32
​
    //被标记为删除的timer,要么是我们调用stop,要么是timer 自己触发后过期导致的删除
    deletedTimers uint32
    ....
}

深入解析go Timer 和Ticker实现原理_第1张图片

为什么是四叉堆

但是与我们常见的,使用二叉树来实现最小堆不同,Golang 这里采用了四叉堆 (4-heap) 来实现。这里 Golang 并没有直接给出解释。 这里直接贴一段 知乎网友对二叉堆和 N 叉堆的分析

  1. 上推节点的操作更快。假如最下层某个节点的值被修改为最小,同样上推到堆顶的操作,N 叉堆需要的比较次数只有二叉堆的

倍。

    1. 对缓存更友好。二叉堆对数组的访问范围更大,更加随机,而 N 叉堆则更集中于数组的前部,这就对缓存更加友好,有利于提高性能。

C 语言知名开源网络库 libev,其timer定时器实现可以在编译时选择采用四叉堆。在它的注释里提到四叉树相比来说缓存更加友好。 根据benchmark,在 50000 + 个 timer 的场景下,四叉树会有 5% 的性能优势。具体可见 libev/ev.c#L2227

NewTimer

//创建一个将会在Duration 时间后将那一刻的时间发生到C 的timer
func NewTimer(d Duration) *Timer {
   c := make(chan Time, 1)  //创建1个channel
   t := &Timer{ //创建一个timer
      C: c,
      r: runtimeTimer{
         when: when(d), //什么时候执行
         f:    sendTime,  //到时候执行的回调函数
         arg:  c,//执行参数
      },
   }
   startTimer(&t.r) //开始timer
   return t
}
  • C 是一个带1个容量的chan,这样做有什么好处呢,原因是chan 无缓冲发送数据就会阻塞,阻塞系统协程,这显然是不行的。
  • 回调函数设置为sendTime,执行参数为channel,sendTime就是到点往C 里面发送当前时间的函数

sendTime实现

//c interface{} 就是NewTimer 赋值的参数,就是channel
func sendTime(c interface{}, seq uintptr) {
    select {
    case c.(chan Time) <- Now(): //写不进去的话,C 已满,走default 分支
    default:
    }
}
  • sendTime 是不阻塞的,在Timer 实现里面是不会被阻塞的,因为只写一次数据。但是在Ticker里面就会存在阻塞,因为容量为1,ticker 会按时间间隔周期性的写数据到C,这时候如果没有写进去,这次写事件就会丢弃。那么是怎么做到呢?
    case c.(chan Time) <- Now() 的时候,如果C 里面的数据没人取走,那么C 已满,case 这条分支发送数据到C就会执行失败而走下面的default。相当于本次调用没有任何操作。
  • 官方注释说:如果reader读C数据慢于第二次向C写数据,那么丢掉这次数据是理想的行为。

After

和NewTimer 一样 ,只是NewTimer 的语法糖,将创建的timer 的C返回了,这样就可以直接使用了

​
func After(d Duration) <-chan Time {
   return NewTimer(d).C
}
  • 和 NewTimer(d).C 是等价的,等待Duration 后将触发,将当前时间写入到返回的 channel。
  • 在计时器触发前,GC 是不会回收timer 的(内存泄漏的提醒,遇到问题时不要怪go 坑,官方可有说明~///(^v^)\\~),不再需要timer时,调用NewTimer 创建timer,然后去调用Timer.Stop效率更高。

AfterFunc

这个函数代表在多少秒后执行传入的回调函数f,并返回timer

​
func AfterFunc(d Duration, f func()) *Timer {
   t := &Timer{
      r: runtimeTimer{
         when: when(d),//什么时候触发
         f:    goFunc, //到时候执行的回调函数
         arg:  f//参数为回调函数
      },
   }
   startTimer(&t.r)
   return t
}
//执行的回调函数
func goFunc(arg interface{}, seq uintptr) {
   go arg.(func())()
}
  • AfterFunc 等待Duration 后触发,在自己goroutine 触发执行回调函数f,返回可以通过调用stop 去放弃执行的timer。
  • 最终执行的函数是goFunc,参数为用户传入的回调函数,当时间一到,启动goroutine 去执行任务。所以用这个执行回调函数修改共享数据很可能产生并发问题。

stop

func (t *Timer) Stop() bool {
   if t.r.f == nil {
      panic("time: Stop called on uninitialized Timer")
   }
   return stopTimer(&t.r) //调用系统的 stopTimer
}
  • 从激活中阻止timer
  • 当timer stop 时返回true,如果timer 已经过期(就是已经被激活)或者已经停止那么则返回false
 package main
​
import (
    "fmt"
    "time"
)
​
//timer 正常stop
func Test1()  {
    timer:=time.NewTimer(1*time.Second)
    fmt.Println(timer.Stop()) //true
}
//timer 已调用stop
func Test2()  {
    timer:=time.NewTimer(1*time.Second)
    timer.Stop()
    fmt.Println(timer.Stop()) //false
}
//timer 已被激活
func Test3()  {
    timer:=time.NewTimer(1*time.Second)
    <-timer.C
    fmt.Println(timer.Stop())//false
}
func main()  {
    Test1()
    Test2()
    Test3()
​
}
​
  • stop 不会关闭channel,去阻止从channel 成功的读取是完全不正确的
  • 确认channel 是空的在调用stop 后,检查返回值然后排空channel.
    例如:假设t.c 里面的值还没被接收
    if !t.Stop() {
        <-t.C
    }
  • 注意事项:不能并发的从channel 里面读数据,也不能并发的调用stop ,我们来看看会发生什么事。因为并发的读chan,当chan 读完1 个数据以后,没有发送者了就会一直阻塞,造成死锁
package main
​
import (
    "time"
)
​
func main()  {
    timer:=time.NewTimer(1*time.Second)
    for i:=0;i<10;i++{
        <-timer.C
    }
}
//结果
//fatal error: all goroutines are asleep - deadlock!
​
goroutine 1 [chan receive]:
main.main()
    D:/code/leetcode/timer.go:10 +0x59
​

来看看并发的调用stop

package main
​
import (
    "fmt"
    "time"
)
​
func main()  {
    timer:=time.NewTimer(1*time.Second)
    for i:=0;i<100;i++{
        go fmt.Println(timer.Stop())
    }
}
//虽然没有错误产生,但是也是不可取的,多次执行返回的状态却不一定是正确的.
false
false
false
true //顺序不固定,多次执行结果不一样
false
  • AfterFunc(d, f) 创建的timer 调用t.stop 返回false ,说明timer 过期了,f 在自己的goroutine 开始执行了,并且stop 不会等待f 执行完才返回,如果想知道f 是否已经完成,则需要自己明确的协商定义,自己去获取状态,例如下面的操作。根据结果可以看到,当timer 已经激活,timer.Stop返回false,但是f 并发没有执行完,我们如果想知道f 的状态,则需要自己操作获取状态。
package main
​
import (
    "fmt"
    "time"
)
​
func main()  {
    var isComplete bool
    timer:=time.AfterFunc(1*time.Second, func() {
        time.Sleep(5*time.Second)
        fmt.Println("任务完成")
        isComplete=true //此例子不是并发操作
    })
    time.Sleep(2*time.Second) //让timer 过期激活
    fmt.Println(timer.Stop(),"isComplete=",isComplete) //true isComplete= false
    for {
        if isComplete{
            break
        }
​
    }
    fmt.Println("任务完成",isComplete)
}
//下面是执行结果
//false isComplete= false
//任务完成
//任务完成 true
​

2 ticker

Ticker 形容时钟滴答滴答的声音,在go 中常用来做定时任务,任务到了执行任务。

Ticker 使用案例,常用来做定时任务或者顶层连接心跳,每秒定时做什么

package main
​
import (
    "fmt"
    "time"
)
​
func main()  {
    t:=time.NewTicker(1*time.Second)
    defer t.Stop()
    for now:=range t.C{
        fmt.Println(now)
    }
}
​

Ticker 结构

​
type Ticker struct {
   C <-chan Time  //chan 定时到了以后,go 系统会忘里面添加一个当前时间的数据
   r runtimeTimer 
}

创建一个Ticker

func NewTicker(d Duration) *Ticker {
   if d <= 0 {
      panic(errors.New("non-positive interval for NewTicker"))
   }
  //这里预留一个缓冲给timer 一样,但是满了以后没人接收后面会丢掉事件
   c := make(chan Time, 1)
   t := &Ticker{
      C: c,
      r: runtimeTimer{
         when:   when(d),
         period: int64(d),
         f:      sendTime, //和ticker 的函数一样
         arg:    c,
      },
   }
   startTimer(&t.r)
   return t
}
  • 和timer 创建方式一样,只不过period为Duration,这样底层在检查时会根据这个字段判断是不是周期性timer,从而删掉原来的timer,创建新的timer

stop

调用stopTimer停止ticker,停止不会关闭 channel。channel也不能被并发读

func (t *Ticker) Stop() {
   stopTimer(&t.r)
}

Reset

调用modTimer修改时间,接下来的激活将在新period后

func (t *Ticker) Reset(d Duration) {
   if t.r.f == nil {
      panic("time: Reset called on uninitialized Ticker")
   }
   modTimer(&t.r, when(d), int64(d), t.r.f, t.r.arg, t.r.seq)
}

Tick

  • 返回ticker 的channel,和timer 一样,对于不需要关闭timer 的客户端有用,但是注意这里没有一个方式去关闭底层timer,所以也不会被垃圾回收
func Tick(d Duration) <-chan Time {
   if d <= 0 {
      return nil
   }
   return NewTicker(d).C
}

3 sleep

func timeSleep(ns int64) {
   if ns <= 0 {
      return
   }
​
   gp := getg()
   t := gp.timer
   if t == nil {
      t = new(timer)
      gp.timer = t
   }
   t.f = goroutineReady
   t.arg = gp
   t.nextwhen = nanotime() + ns
   if t.nextwhen < 0 { // check for overflow.
      t.nextwhen = maxWhen
   }
   gopark(resetForSleep, unsafe.Pointer(t), waitReasonSleep, traceEvGoSleep, 1)
}
​
func resetForSleep(gp *g, ut unsafe.Pointer) bool {
   t := (*timer)(ut)
   resettimer(t, t.nextwhen)
   return true
}
  • sleep底层也是创建timer,执行函数为goroutineReady,参数为当前g,执行时间为当前时间+传进来的时间,然后调用gopark。停止协程,等待timer 过期被唤醒。
  • resetForSleep 在goroutine停止后调用,我们不能自己调用resettimer,因为如果这是短暂的sleep,并且有大量goroutines,p 可能在goroutine被停止前提交调用goroutineReady

4 底层源码解析

注意:本文为go 在1.17.2 下面的源码解析,如果是1.15 或者1.16 可能有些不同

来看看下面这些函数,go/src/time 是找不到任何实现的,其实go 官方包一个共性,找不到的都在rumtime里面,我们去runtime 里面找找。

func startTimer(*runtimeTimer)
func stopTimer(*runtimeTimer) bool
func resetTimer(*runtimeTimer, int64) bool
func modTimer(t *runtimeTimer, when, period int64, f func(interface{}, uintptr), arg interface{}, seq uintptr)

在go/src/runtime/time.go,我们发现了这些函数的实现,下面我们来看看源码。

// startTimer adds t to the timer heap.
//go:linkname startTimer time.startTimer
func startTimer(t *timer) {
	if raceenabled {
		racerelease(unsafe.Pointer(t))
	}
	addtimer(t)
}

// stopTimer stops a timer.
// It reports whether t was stopped before being run.
//go:linkname stopTimer time.stopTimer
func stopTimer(t *timer) bool {
	return deltimer(t)
}

// resetTimer resets an inactive timer, adding it to the heap.
//go:linkname resetTimer time.resetTimer
// Reports whether the timer was modified before it was run.
func resetTimer(t *timer, when int64) bool {
	if raceenabled {
		racerelease(unsafe.Pointer(t))
	}
	return resettimer(t, when)
}

// modTimer modifies an existing timer.
//go:linkname modTimer time.modTimer
func modTimer(t *timer, when, period int64, f func(interface{}, uintptr), arg interface{}, seq uintptr) {
	modtimer(t, when, period, f, arg, seq)
}
  • 下面这些是函数的真正实现,通过go:linkname链接到time里面的函数,所以可以官方源码看到很多时候go 函数并没有实现,但是能调用

timer 的状态字段

const (
    // 默认状态,没有状态
    timerNoStatus = iota
​
    // 等待timer 去触发,在P 的堆上
    timerWaiting
​
    // 执行 timer function.timer 只会短暂的拥有这个状态
    timerRunning
​
    // timer 被删了,应该被从堆里面移除。
    // 在这个状态下不要执行,但是任然在P 的堆上。
    timerDeleted
​
    // timer 将被移除
    // 这个状态也是短暂的
    timerRemoving
​
    // timer 已经被移除了,并且不在P 的堆上面
    timerRemoved
​
    // The timer 将被修改,这个状态也是短暂的
    timerModifying
​
    // timer 被修改成更早的时间了,新的执行时间when 在nextWhen 上面
    // timer 在P 的堆上, 可能在错误的地方.
    timerModifiedEarlier
​
    // timer 修改成更后面的时间了,新的执行时间when 在nextWhen 上面
    // timer 在P 的堆上, 可能在错误的地方.
    timerModifiedLater
​
    // timer 被修改并且将被移除。该状态是短暂的
​
    timerMoving
)

官方给的操作时的状态转换

//
// addtimer:
//   timerNoStatus   -> timerWaiting
//   anything else   -> panic: invalid value
// deltimer:
//   timerWaiting         -> timerModifying -> timerDeleted
//   timerModifiedEarlier -> timerModifying -> timerDeleted
//   timerModifiedLater   -> timerModifying -> timerDeleted
//   timerNoStatus        -> do nothing
//   timerDeleted         -> do nothing
//   timerRemoving        -> do nothing
//   timerRemoved         -> do nothing
//   timerRunning         -> wait until status changes
//   timerMoving          -> wait until status changes
//   timerModifying       -> wait until status changes
// modtimer:
//   timerWaiting    -> timerModifying -> timerModifiedXX
//   timerModifiedXX -> timerModifying -> timerModifiedYY
//   timerNoStatus   -> timerModifying -> timerWaiting
//   timerRemoved    -> timerModifying -> timerWaiting
//   timerDeleted    -> timerModifying -> timerModifiedXX
//   timerRunning    -> wait until status changes
//   timerMoving     -> wait until status changes
//   timerRemoving   -> wait until status changes
//   timerModifying  -> wait until status changes
// cleantimers (looks in P's timer heap):
//   timerDeleted    -> timerRemoving -> timerRemoved
//   timerModifiedXX -> timerMoving -> timerWaiting
// adjusttimers (looks in P's timer heap):
//   timerDeleted    -> timerRemoving -> timerRemoved
//   timerModifiedXX -> timerMoving -> timerWaiting
// runtimer (looks in P's timer heap):
//   timerNoStatus   -> panic: uninitialized timer
//   timerWaiting    -> timerWaiting or
//   timerWaiting    -> timerRunning -> timerNoStatus or
//   timerWaiting    -> timerRunning -> timerWaiting
//   timerModifying  -> wait until status changes
//   timerModifiedXX -> timerMoving -> timerWaiting
//   timerDeleted    -> timerRemoving -> timerRemoved
//   timerRunning    -> panic: concurrent runtimer calls
//   timerRemoved    -> panic: inconsistent timer heap
//   timerRemoving   -> panic: inconsistent timer heap
//   timerMoving     -> panic: inconsistent timer heap
​

timer 的数据结构与算法

堆排序算法

  • p 的timers是个数组,经过堆排序后维护,所用的堆是4叉小顶堆,每个p 都有一个这样的堆。
  • 每次堆顶的timer 的when 都是最小的代表最先执行timer,所以我们只要不断循环获取堆顶这个timer 执行就行了。

siftupTimer

  • 堆维护算法,手动的检查切片索引是否合法,切片索引错误通常发生在不优雅的访问timers上面,但是我们不能panic,因为这会引起严重的问题 ”在锁中panic “,相应的,我们panic 在没有上锁的时候。
  • 堆上滤算法,将i 位置的timer 上滤,插入到合适的位置,就是和父节点比大小,如果小,则和父节点交换,直到找到合适的位置(比父节点大),注意这是四叉堆,所以父节点是(i - 1) / 4
func siftupTimer(t []*timer, i int) int {
   if i >= len(t) {
      badTimer()
   }
   when := t[i].when
   if when <= 0 {
      badTimer()
   }
   tmp := t[i]
   for i > 0 {
      p := (i - 1) / 4 // parent
      if when >= t[p].when {
         break
      }
      t[i] = t[p]
      i = p
   }
   if tmp != t[i] {
      t[i] = tmp
   }
   return i
}

siftdownTimer

  • 堆算法下滤将上面的timer 不断往下找,直到找到合适的位置。
  • 不断比较和child 的执行时间,找到4个child 执行时间更小的不断交换,直到找到合适的位置。
  • 子child 分别是i*4 + 1,i*4 + 2,i*4 + 3,i*4 + 4
func siftdownTimer(t []*timer, i int) {
   n := len(t)
   if i >= n {
      badTimer()
   }
   when := t[i].when
   if when <= 0 {
      badTimer()
   }
   tmp := t[i]
   for {
      c := i*4 + 1 // left child
      c3 := c + 2  // mid child //第三个孩子
      if c >= n {
         break
      }
      w := t[c].when //先让w 获取第一个孩子的执行时间
      if c+1 < n && t[c+1].when < w { //如果第二个孩子执行时间更小将w 换成第二个孩子
         w = t[c+1].when
         c++
      }
      if c3 < n {
         w3 := t[c3].when
         if c3+1 < n && t[c3+1].when < w3 { //再来比较低三个孩子和第四个孩子
            w3 = t[c3+1].when
            c3++
         }
         if w3 < w {//最后选出执行时间最小的孩子
            w = w3
            c = c3
         }
      }
      if w >= when {
         break
      }
      t[i] = t[c]
      i = c
   }
   if tmp != t[i] {
      t[i] = tmp
   }
}

acquirem

在之前我们先聊聊这个函数,禁止抢占

//go:nosplit
func acquirem() *m {
   _g_ := getg()
   _g_.m.locks++
   return _g_.m
}

抢占时机判断

在判断m 是否可以被抢占的时候,有下面几个条件

//C:\Go\src\runtime\preempt.go +287
func canPreemptM(mp *m) bool {
   return mp.locks == 0 && mp.mallocing == 0 && mp.preemptoff == "" && mp.p.ptr().status == _Prunning
}
  • 运行时没有禁止抢占(m.locks == 0
  • 运行时没有在执行内存分配(m.mallocing == 0
  • 运行时没有关闭抢占机制(m.preemptoff == ""
  • M 与 P 绑定且没有进入系统调用(p.status == _Prunning

所以acquirem将m.locks++,禁止抢占,调用mp.locks--,恢复可以抢占

//go:nosplit
func releasem(mp *m) {
   _g_ := getg()
   mp.locks--
   if mp.locks == 0 && _g_.preempt {
      // restore the preemption request in case we've cleared it in newstack
      _g_.stackguard0 = stackPreempt
   }
}

addtimer

使用:

深入解析go Timer 和Ticker实现原理_第2张图片

  • 添加一个timer,只能用在新建一个timer的时候,避免在p 的堆上改变timer的when 字段 而造成堆是无序的
func addtimer(t *timer) {
    // when must be positive. A negative value will cause runtimer to
    // overflow during its delta calculation and never expire other runtime
    // timers. Zero will cause checkTimers to fail to notice the timer.
    if t.when <= 0 {
        throw("timer when must be positive")
    }
    if t.period < 0 {
        throw("timer period must be non-negative")
    }
    if t.status != timerNoStatus {
        throw("addtimer called with initialized timer")
    }
    t.status = timerWaiting
​
    when := t.when
    //禁用p被抢占去避免去改变其他p 的堆栈
    // Disable preemption while using pp to avoid changing another P's heap.
    mp := acquirem()
​
    pp := getg().m.p.ptr() //获取当前p
    lock(&pp.timersLock)
    cleantimers(pp) //清除timers
    doaddtimer(pp, t)//添加timer
    unlock(&pp.timersLock)
​
    wakeNetPoller(when)//添加到netpoller
​
    releasem(mp)
}
  • 就是将timer 加到当前执行p的timers数组里面去
  • 整个过程中需要设置不可抢占,为什么需要设置为M 不可抢占了,因为如果被抢占了,此时操作的p 有可能发生变化,造成操作别人的堆。
  • 调用 wakeNetPoller 方法:唤醒网络轮询器中休眠的线程,检查计时器被唤醒的时间(when)是否在当前轮询预期运行的时间(pollerPollUntil)内,若是唤醒。

doaddtimer

将timer 添加到当前p 的堆上,该函数在锁中执行

​
func doaddtimer(pp *p, t *timer) {
   // Timers 通过netpoll 恢复执行
   if netpollInited == 0 { //netpoll 没有初始化,重新初始化netpoll
      netpollGenericInit()
   }
​
   if t.pp != 0 {
      throw("doaddtimer: P already set in timer")
   }
   t.pp.set(pp) //设置timer 的pp 
   i := len(pp.timers)
   pp.timers = append(pp.timers, t) //添加到timers,也是就是添加到堆尾
   siftupTimer(pp.timers, i)//上滤
   if t == pp.timers[0] {//如果是堆顶的话,还需要重新更新pp.timer0When
      atomic.Store64(&pp.timer0When, uint64(t.when))
   }
   atomic.Xadd(&pp.numTimers, 1) //将总timer 数量+1
}

deltimer

使用:可见在netpoll 或者stopTimer 会使用该函数去将timer 状态标记为被删除timer

深入解析go Timer 和Ticker实现原理_第3张图片

func deltimer(t *timer) bool {
   for {
      switch s := atomic.Load(&t.status); s {
      case timerWaiting, timerModifiedLater:
         // Prevent preemption while the timer is in timerModifying.
         // This could lead to a self-deadlock. See #38070.
         mp := acquirem()
         if atomic.Cas(&t.status, s, timerModifying) {
             //必须在改变状态前获取到pp,因为其他goroutine可能 清理掉将timerDeleted timer
            tpp := t.pp.ptr()
            if !atomic.Cas(&t.status, timerModifying, timerDeleted) {
               badTimer()
            }
            releasem(mp)
            atomic.Xadd(&tpp.deletedTimers, 1)
            // Timer was not yet run.
            return true
         } else {
            releasem(mp)
         }
      case timerModifiedEarlier:
         // Prevent preemption while the timer is in timerModifying.
         // This could lead to a self-deadlock. See #38070.
         mp := acquirem()
         if atomic.Cas(&t.status, s, timerModifying) {
            // Must fetch t.pp before setting status
            // to timerDeleted.
            tpp := t.pp.ptr()
            if !atomic.Cas(&t.status, timerModifying, timerDeleted) {
               badTimer()
            }
            releasem(mp)
            atomic.Xadd(&tpp.deletedTimers, 1)
            // Timer was not yet run.
            return true
         } else {
            releasem(mp)
         }
      case timerDeleted, timerRemoving, timerRemoved:
         // Timer was already run.
         return false
      case timerRunning, timerMoving:
         // The timer is being run or moved, by a different P.
         // Wait for it to complete.
         osyield()
      case timerNoStatus:
         // Removing timer that was never added or
         // has already been run. Also see issue 21874.
         return false
      case timerModifying:
         // Simultaneous calls to deltimer and modtimer.
         // Wait for the other call to complete.
         osyield()
      default:
         badTimer()
      }
   }
}
  • 简单来说就是修改timer 的状态,先修改为将被修改状态,再修改为删除状态
  • 因为都是操作timer.p,而且修改p.timers这个操作是可能有多个goroutine操作,所以我们要先获取timer.p 然后将被删除的timer 数量+1。如果没有在操作之前获取到timer.p,后面被标记为删除被其他goroutine清理掉了,那么我们就再也获取不到timer.p了。因为timer.p被置为0了。

cleantimers

  • 加快了创建和删除计时器的程序的速度,将它们留在堆中会减慢添加时间,也报告有问题的timer,必须在pp上面加锁
​
func cleantimers(pp *p) {
   gp := getg() //获取执行的gp
   for {
      if len(pp.timers) == 0 { //如果没有timers 返回
         return
      }
      //从理论上来讲这个循环将会运行一段时间,因为加了timersLock不能被抢占,如果某人想抢占这个goroutine,我们返回等待下一次清理
      if gp.preemptStop {
         return
      }
​
      t := pp.timers[0] //获取堆顶元素
      if t.pp.ptr() != pp {
         throw("cleantimers: bad p")
      }
      switch s := atomic.Load(&t.status); s {
      case timerDeleted: //被删除了,先将状态改为待移除
         if !atomic.Cas(&t.status, s, timerRemoving) {
            continue
         }
          //清除堆顶元素
         dodeltimer0(pp)
          //将状态改为移除
         if !atomic.Cas(&t.status, timerRemoving, timerRemoved) {
            badTimer()
         }
          //将被删除的timers 数量减一
         atomic.Xadd(&pp.deletedTimers, -1)
      case timerModifiedEarlier, timerModifiedLater:
          //时间修改为更早或者更晚,将状态改为移除
         if !atomic.Cas(&t.status, s, timerMoving) {
            continue
         }
         // 将执行时间改为下一次移除
         t.when = t.nextwhen
         dodeltimer0(pp) //先删除堆顶
         doaddtimer(pp, t)//重新加入timer
         if !atomic.Cas(&t.status, timerMoving, timerWaiting) { //将状态置为等待触发
            badTimer()
         }
      default:
         // Head of timers does not need adjustment.
         return
      }
   }
}

dodeltimer

真正执行删除

func dodeltimer(pp *p, i int) int {
   if t := pp.timers[i]; t.pp.ptr() != pp {
      throw("dodeltimer: wrong P")
   } else {
      t.pp = 0
   }//判断timer 的状态
   last := len(pp.timers) - 1
   if i != last {
      pp.timers[i] = pp.timers[last]
   }//将timer 和对最后一个timer 交换
   pp.timers[last] = nil
   pp.timers = pp.timers[:last]
   smallestChanged := i
   if i != last { //i 等于last ,代表移除最后一个timer.不需要下滤或者上滤
     //先上滤,然后下滤,这样保证有序了
      smallestChanged = siftupTimer(pp.timers, i)
      siftdownTimer(pp.timers, i)
   }
   if i == 0 { //更改堆顶元素需要,更新pp 上面堆顶元素状态
      updateTimer0When(pp)
   }
   atomic.Xadd(&pp.numTimers, -1) //总数量减一
   return smallestChanged
}

dodeltimer0

删除堆顶的timer

func dodeltimer0(pp *p) {
   if t := pp.timers[0]; t.pp.ptr() != pp {
      throw("dodeltimer0: wrong P")
   } else {
      t.pp = 0
   }
   last := len(pp.timers) - 1
   if last > 0 {
      pp.timers[0] = pp.timers[last]
   }
   pp.timers[last] = nil
   pp.timers = pp.timers[:last]
   if last > 0 {
      siftdownTimer(pp.timers, 0)
   }
   updateTimer0When(pp)
   atomic.Xadd(&pp.numTimers, -1)
}
  • 取出堆顶的timer ,然后判端timer 所属pp 和当前执行的pp 是不是一样的,如果一样,则将timer的pp 置为0
    因为堆顶timer 删除,但是timer 还是有引用的,将pp 置为0,代表该timer 不属于任何p了。
  • 执行的堆顶删除流程
    1、将堆顶和堆最后一个元素交换,将最后一个元素置为nil,将数组重新分配删除最后一个,下面这两句话可以保证被删除的元素垃圾回收,如果没有这句pp.timers[last] = nil,可能会内存泄露,因为最后一个元素有引用不会被回收。
    pp.timers[last] = nil pp.timers = pp.timers[:last]
    2、执行下滤操作

updateTimer0When

if len(pp.timers) == 0 {
   atomic.Store64(&pp.timer0When, 0)
} else {
   atomic.Store64(&pp.timer0When, uint64(pp.timers[0].when))
}

该字段将会更新pp 上面堆顶元素的执行时间,因为原来的被删除了,此时应该用一个新的去执行

modtimer

  • 修改一个存在的timer
  • 被调用在time.Ticker.Reset or time.Timer.Reset或者netpoll 上面
func modtimer(t *timer, when, period int64, f func(interface{}, uintptr), arg interface{}, seq uintptr) bool {
   if when <= 0 { //检验时间
      throw("timer when must be positive")
   }
   if period < 0 {
      throw("timer period must be non-negative")
   }

   status := uint32(timerNoStatus)
   wasRemoved := false
   var pending bool
   var mp *m
loop:
   for {
      switch status = atomic.Load(&t.status); status {
      case timerWaiting, timerModifiedEarlier, timerModifiedLater:
         // 禁止p 被抢占
         // This could lead to a self-deadlock. See #38070.
         mp = acquirem()
          //将状态置为待修改
         if atomic.Cas(&t.status, status, timerModifying) {
            pending = true // timer not yet run
            break loop
         }
         releasem(mp)
      case timerNoStatus, timerRemoved:
         // 禁止p 被抢占
         // This could lead to a self-deadlock. See #38070.
         mp = acquirem()

         // 禁止p 被抢占
         // Act like addtimer.
         if atomic.Cas(&t.status, status, timerModifying) {
            wasRemoved = true
            pending = false // timer already run or stopped
            break loop
         }
         releasem(mp)
      case timerDeleted:
         // 禁止p 被抢占
         // This could lead to a self-deadlock. See #38070.
         mp = acquirem()
         if atomic.Cas(&t.status, status, timerModifying) {
            atomic.Xadd(&t.pp.ptr().deletedTimers, -1)
            pending = false // timer 已经运行或者停止了
            break loop
         }
         releasem(mp)
      case timerRunning, timerRemoving, timerMoving:
         // timer 被当前p 运行或者移除
         // 等待状态完成
         osyield()
      case timerModifying:
         // modtimer被多个同时调用
         // 等待状态完成
         osyield()
      default:
         badTimer()
      }
   }

   t.period = period
   t.f = f
   t.arg = arg
   t.seq = seq

   if wasRemoved { //如果timer 被移除了
      t.when = when
      pp := getg().m.p.ptr()
      lock(&pp.timersLock)
      doaddtimer(pp, t) //添加这个timer,注意要加锁,因为不知道哪个p 在操作,会有数据竞争
      unlock(&pp.timersLock)
      if !atomic.Cas(&t.status, timerModifying, timerWaiting) { //将timer 修改为等待
         badTimer()
      }
      releasem(mp)
      wakeNetPoller(when)//加入netpoller 等待唤醒
   } else {//如果没有被移除,因为timer 在某些p 的堆上,所以我们不能改变这个字段,如果我们改变了,其他p 的堆将会无序了,所以我们赋值给nextwhen ,让其他p 自己去操作when 字段去重排序堆
      t.nextwhen = when

      newStatus := uint32(timerModifiedLater) //更新状态,根据修改时间判断时修改为更早了还是更晚了
      if when < t.when {
         newStatus = timerModifiedEarlier
      }

      tpp := t.pp.ptr()

      if newStatus == timerModifiedEarlier { //修改为更早了,就更新为更早
         updateTimerModifiedEarliest(tpp, when)
      }

      //更新timerModifying状态
      if !atomic.Cas(&t.status, timerModifying, newStatus) {
         badTimer()
      }
      releasem(mp)

      // If the new status is earlier, wake up the poller.
      if newStatus == timerModifiedEarlier {
         wakeNetPoller(when) //加入netpoller 等待唤醒
      }
   }

   return pending
}

osyield

简单来说就相当于sleep,精度更小,暂停一些,等待其他协程执行

该函数在不同平台的实现不一样

linux(src\runtime\os_linux.go +411))

空实现,简单来说就是不做任何事情

func osyield()

mac(src\runtime\os_darwin.go +347)

//go:nosplit
func osyield() {
   usleep(1)
}
​
//go:nosplit
//高精度的timer 可以用,优先使用高优先级的,不能话就只能使用低优先级的
func usleep(us uint32) {
    systemstack(func() {
        dt := -10 * int32(us) // relative sleep (negative), 100ns units
        // If the high-res timer is available and its handle has been allocated for this m, use it.
        // Otherwise fall back to the low-res one, which doesn't need a handle.
        if haveHighResTimer && getg().m.highResTimer != 0 {
            usleep2HighRes(dt)
        } else {
            usleep2(dt)
        }
    })
}

windows(src\runtime\os_darwin.go +1157)

//go:nosplit
func osyield() {
   systemstack(switchtothread)
}
​
// systemstack runs fn on a system stack.
//在系统栈上(g0)调用fn或者在gsignal 栈
//调用并且直接返回
// Otherwise, systemstack is being called from the limited stack
// of an ordinary goroutine. In this case, systemstack switches
// to the per-OS-thread stack, calls fn, and switches back.
// It is common to use a func literal as the argument, in order
// to share inputs and outputs with the code around the call
// to system stack:
//
//  ... set up y ...
//  systemstack(func() {
//      x = bigcall(y)
//  })
//  ... use x ...
//
//go:noescape
func systemstack(fn func())

switchtothread 说明

  • 调用这个系统调用时,系统会查看一个迫切需要cpu时间的线程,如果没有就返回,SwitchToThread就对该线程进行调度(该线程的优先级可能低于调用SwitchToThread的线程)。这个迫切需要CPU时间的线程可以运行一个时间段,然后系统调度程序照常运行。
  • 该函数允许一个需要资源的线程强制另一个优先级较低、而目前却拥有该资源的线程放弃该资源(抢占资源)。如果调用SwitchToThread函数时没有其他线程能够运行,那么该函数返回FALS E,否则返回一个非0值。
  • SwitchToThread允许执行低优先级线程,Sleep会立即重新调度主调线程,即使低优先级线程会处于饥饿状态

updateTimerModifiedEarliest

深入解析go Timer 和Ticker实现原理_第4张图片

  • 修改p 记录最早运行时间的字段
// updateTimerModifiedEarliest updates the recorded nextwhen field of the
// earlier timerModifiedEarier value.
// The timers for pp will not be locked.
func updateTimerModifiedEarliest(pp *p, nextwhen int64) {
   for {
      old := atomic.Load64(&pp.timerModifiedEarliest)
      if old != 0 && int64(old) < nextwhen {
         return
      }
      if atomic.Cas64(&pp.timerModifiedEarliest, old, uint64(nextwhen)) {
         return
      }
   }
}

resettimer

// resettimer resets the time when a timer should fire.
// If used for an inactive timer, the timer will become active.
// This should be called instead of addtimer if the timer value has been,
// or may have been, used previously.
// Reports whether the timer was modified before it was run.
func resettimer(t *timer, when int64) bool {
   return modtimer(t, when, t.period, t.f, t.arg, t.seq)
}
  • 调用修改timer ,将参数传进去

moveTimers

调用于C:\Go\src\runtime\proc.go+4926

深入解析go Timer 和Ticker实现原理_第5张图片

  • 将一个p 上的timer 移动到另一个p 上
  • 被调用在STW上面,但是调用者期望pp 已经给timers 加锁
// moveTimers moves a slice of timers to pp. The slice has been taken
// from a different P.
// This is currently called when the world is stopped, but the caller
// is expected to have locked the timers for pp.
func moveTimers(pp *p, timers []*timer) {
   for _, t := range timers { //循环遍历timer 
   loop:
      for {
         switch s := atomic.Load(&t.status); s {
         case timerWaiting: //如果timer 是将运行的,将timer 状态置为移动
            if !atomic.Cas(&t.status, s, timerMoving) {
               continue
            }
            t.pp = 0 //状态改成功后,添加timer 也成功了,将状态重新置为待触发
            doaddtimer(pp, t)
            if !atomic.Cas(&t.status, timerMoving, timerWaiting) {
               badTimer()
            }
            break loop
         case timerModifiedEarlier, timerModifiedLater:
              //如果timer 是时间被修改的,将timer 状态置为移动
            if !atomic.Cas(&t.status, s, timerMoving) {
               continue
            }
            t.when = t.nextwhen//更新时间
            t.pp = 0//状态改成功后,添加timer 也成功了,将状态重新置为待触发
            doaddtimer(pp, t)
            if !atomic.Cas(&t.status, timerMoving, timerWaiting) {
               badTimer()
            }
            break loop
         case timerDeleted: //timer是已经被删除的将timer 状态置为移动
            if !atomic.Cas(&t.status, s, timerRemoved) {
               continue
            }
            t.pp = 0 但是pp 置为0
            break loop
         case timerModifying:
            // Loop until the modification is complete.
            osyield()
         case timerNoStatus, timerRemoved:
           
            badTimer()
         case timerRunning, timerRemoving, timerMoving:
            // Some other P thinks it owns this timer,
            // which should not happen.
            badTimer()
         default:
            badTimer()
         }
      }
   }
}
  • 总结来说就是先将状态置为待移除,然后重新添加到新p的堆上

应用

在p 被销毁时,将会调用这个函数将timer移动到其他p 上面去

func (pp *p) destroy() {
    ....
    if len(pp.timers) > 0 {
		plocal := getg().m.p.ptr()
        //此时已经进入stw了,但是我们任然需要加锁去避免监控调用timeSleepUntil函数,发生在多个p 的情况下,所以不用担心死锁
		lock(&plocal.timersLock)
		lock(&pp.timersLock)
		moveTimers(plocal, pp.timers) //将销毁的p 的timers 移动到当前执行goroutine 的p 上面
		pp.timers = nil
		pp.numTimers = 0
		pp.deletedTimers = 0
		atomic.Store64(&pp.timer0When, 0)
		unlock(&pp.timersLock)
		unlock(&plocal.timersLock)
	}
    .....
}

checkTimers

checkTimers 运行 P 准备好的任意一个timer

/usr/local/go/src/runtime/proc.go +3423

func checkTimers(pp *p, now int64) (rnow, pollUntil int64, ran bool) {
   //加载第一个timer 什么时候执行,加载第一个timer 是否修改为更早的时间了
   next := int64(atomic.Load64(&pp.timer0When))
   nextAdj := int64(atomic.Load64(&pp.timerModifiedEarliest))
    //如果next=0或者被修改为更早的时间了,则将next 修改为nextAdj
   if next == 0 || (nextAdj != 0 && nextAdj < next) {
      next = nextAdj
   }
​
   if next == 0 {
      //没有修改或者重新修正timer
      return now, 0, false
   }
​
   if now == 0 {
      now = nanotime()
   }
   if now < next {
       //当前时间小于下次执行的时间,timer 没有准备去运行,但是我们还是要继续去清理被删除的timers,
       //下面的条件决定是否我们需要去清理timer
       //timer 的p 不是当前g 运行的p或者删除的timer 数量没有超过总timer 数量的1/4,注意这里是单个p
      if pp != getg().m.p.ptr() || int(atomic.Load(&pp.deletedTimers)) <= int(atomic.Load(&pp.numTimers)/4) {
         return now, next, false
      }
   }
​
   lock(&pp.timersLock) //加锁,
​
   if len(pp.timers) > 0 {
      adjusttimers(pp, now)
      for len(pp.timers) > 0 {
         // 注意runtimer 可能会短暂的解锁pp.timersLock.
         if tw := runtimer(pp, now); tw != 0 { //进行runtimer 操作,如果tw 大于0,说明没有timer 可以执行,并且堆顶timer的下一次执行时间为tw
            if tw > 0 {
               pollUntil = tw
            }
            break
         }
         ran = true
      }
   }
​
   //与上面条件相反,满足条件我们进进行清理
   if pp == getg().m.p.ptr() && int(atomic.Load(&pp.deletedTimers)) > len(pp.timers)/4 {
      clearDeletedTimers(pp)
   }
​
   unlock(&pp.timersLock)//解锁
​
   return now, pollUntil, ran
}
  • 先将堆顶的timer时间修改为正确的时间,因为timer 执行时间可能修改为更早了
  • 判断是否满足清理被删除timer 的条件,如果不满足直接返回
  • 如果p 上的timer数量大于0,不断的循环选择堆顶的timer,运行或者更新,如果没有可以执行的,将堆顶的timer 执行时间赋值给pollUntil
  • 最后一步判断是否可以清理被删除timer ,如果是就清理timer,这里是让自己p 运行G处理自己上面的timer,可以减少锁竞争,提高效率
  • 运行timer和清理timer都在锁里面进行的,这个锁的粒度是对P 进行加锁。
  • now=0,会在这里获取当前时间赋值给now

runtimer

  • 考察处理堆顶的元素,移除和更新堆顶的timer
  • 返回0,如果timer 开始运行,返回-1 如果堆上没有timers.
  • 调用者必须给pp(处理器) 上的timers加锁,如果timer 已经运行起来了,将会短暂的解锁
func runtimer(pp *p, now int64) int64 {
   for {
      t := pp.timers[0] //获取第0 个timer,堆顶的timer,根据状态做不同的操作
      if t.pp.ptr() != pp {
         throw("runtimer: bad p")
      }
      switch s := atomic.Load(&t.status); s { //原子获取到的timer 的状态
      case timerWaiting: //状态代表准备去运行
         if t.when > now {//但是t.when 大于当前时间,说明没有准备去运行,直接将下次执行时间返回
            // Not ready to run.
            return t.when
         }

         if !atomic.Cas(&t.status, s, timerRunning) { //将状态变为运行中,这个状态是短暂的,continue 原因是因为cas 操作可能失败,所以循环cas 操作
            continue
         }
         runOneTimer(pp, t, now) //运行这个timer,会短暂的接口pp lock
         return 0

      case timerDeleted: //状态是被删除的,就将状态转为待移除
         if !atomic.Cas(&t.status, s, timerRemoving) {
            continue
         }
         dodeltimer0(pp) //删除p第一个timer
         if !atomic.Cas(&t.status, timerRemoving, timerRemoved) {
            badTimer()
         }
         atomic.Xadd(&pp.deletedTimers, -1)
         if len(pp.timers) == 0 {
            return -1
         }

      case timerModifiedEarlier, timerModifiedLater: //时间被修改成更早或者更晚,这时候需要删除堆顶的timer
         if !atomic.Cas(&t.status, s, timerMoving) {
            continue
         }
         t.when = t.nextwhen //将时间更新
         dodeltimer0(pp)//删除堆顶的timer,也就是之前的自己
         doaddtimer(pp, t)//重新将timer 添加进去
         if !atomic.Cas(&t.status, timerMoving, timerWaiting) {//状态改为等待待触发,
            badTimer()
         }

      case timerModifying:
         // Wait for modification to complete.
         osyield()

      case timerNoStatus, timerRemoved: //状态是错误的,不活跃的timer 不会出现在堆上
         badTimer()
      case timerRunning, timerRemoving, timerMoving:
         // These should only be set when timers are locked,
         //这些状态只会出现在timer 被locked 时候
         badTimer()
      default:
         badTimer()
      }
   }
}

runOneTimer

  • 运行单个timer
  • 调用者必须锁住p 的timers
  • 将会短暂的解开timers 的锁在执行函数的时候
​
func runOneTimer(pp *p, t *timer, now int64) {
   if raceenabled {
      ppcur := getg().m.p.ptr()
      if ppcur.timerRaceCtx == 0 {
         ppcur.timerRaceCtx = racegostart(funcPC(runtimer) + sys.PCQuantum)
      }
      raceacquirectx(ppcur.timerRaceCtx, unsafe.Pointer(t))
   }
​
   f := t.f //获取timer执行函数
   arg := t.arg//获取timer 参数
   seq := t.seq//获取timer 参数
​
   if t.period > 0 { //代表这是周期性任务,时间间隔为t.period
      // Leave in heap but adjust next time to fire.
      delta := t.when - now
      t.when += t.period * (1 + -delta/t.period)
      if t.when < 0 { // check for overflow.
         t.when = maxWhen
      }
      siftdownTimer(pp.timers, 0) //堆算法下滤操作,将timer 从timerRunning状态重新置为timerWaiting状态
      if !atomic.Cas(&t.status, timerRunning, timerWaiting) {
         badTimer()
      }
      updateTimer0When(pp)
   } else {
      // 不是周期性任务,删掉timer,然后将timer 的状态改为无状态,等待被回收,不得不说go 官方还是挺仔细的,要是我写返回会被回收,估计就不会去重置timer状态了
      dodeltimer0(pp)
      if !atomic.Cas(&t.status, timerRunning, timerNoStatus) {
         badTimer()
      }
   }
​
   if raceenabled {
      // Temporarily use the current P's racectx for g0.
      gp := getg()
      if gp.racectx != 0 {
         throw("runOneTimer: unexpected racectx")
      }
      gp.racectx = gp.m.p.ptr().timerRaceCtx
   }
​
   unlock(&pp.timersLock)
​
   f(arg, seq)
​
   lock(&pp.timersLock)
​
   if raceenabled {
      gp := getg()
      gp.racectx = 0
   }
}
  • 该函数运行一个timer,如果是周期性任务,先将原来的timer 从堆里面删除掉,然后更新原来的timer时间为下一次运行时间,重新插入堆中。如果是单任务则直接删掉
  • 下面将参数和回调函数获取到,然后直接执行,这里有个细节点,就是解除了p 的锁,其实这也是官方的优化,如果不解除只能保证本p 上的G 运行这个任务,到这里已经不涉及到任何数据竞争了,只是单纯的执行回调函数,让其他p 上的 去抢占执行才是最正确的选择。要不然将会一直占用timer 所属p 的资源。

adjusttimers

  • 查找当前p 上的timers 是否有被修改为更早时间的,找到把它放在堆上正确的位置
  • 它也移动将会修改到后面运行的timers ,或者移除被删除的timers
  • 操作必须加锁
func adjusttimers(pp *p, now int64) {
   // If we haven't yet reached the time of the first timerModifiedEarlier
   // timer, don't do anything. This speeds up programs that adjust
   // a lot of timers back and forth if the timers rarely expire.
   // We'll postpone looking through all the adjusted timers until
   // one would actually expire.
   first := atomic.Load64(&pp.timerModifiedEarliest)
   if first == 0 || int64(first) > now {
      if verifyTimers {
         verifyTimerHeap(pp)
      }
      return
   }
​
   // We are going to clear all timerModifiedEarlier timers.
   atomic.Store64(&pp.timerModifiedEarliest, 0)
​
   var moved []*timer
   for i := 0; i < len(pp.timers); i++ { //循环遍历该p 上的所有timer
      t := pp.timers[i]
      if t.pp.ptr() != pp {
         throw("adjusttimers: bad p")
      }
      switch s := atomic.Load(&t.status); s {
      case timerDeleted: //是删除的话,将状态改为将移除
         if atomic.Cas(&t.status, s, timerRemoving) {
            changed := dodeltimer(pp, i) //删除该位置的timer
            if !atomic.Cas(&t.status, timerRemoving, timerRemoved) { //将状态改为已移除
               badTimer()
            }
            atomic.Xadd(&pp.deletedTimers, -1) //已标记删掉的timers -1
            // 在for 循环里面删掉了这个timer,此时的i 会变了,获取正确位置的i
            i = changed - 1
         }
      case timerModifiedEarlier, timerModifiedLater:
         if atomic.Cas(&t.status, s, timerMoving) {
           //将timer 执行时间修改为正确的执行时间
            t.when = t.nextwhen
             //将timer 从堆里面拿起来,我们不在这里做是因为添加会改变timer 在堆里面位置,在遍历完在重新添加。
            changed := dodeltimer(pp, i)
            moved = append(moved, t)
            //获取正确的i 索引
            i = changed - 1
         }
      case timerNoStatus, timerRunning, timerRemoving, timerRemoved, timerMoving:
         badTimer()
      case timerWaiting:
         // OK, nothing to do.
      case timerModifying:
         // Check again after modification is complete.
         osyield()//等一下
         i-- //将i-- ,重复上一步操作
      default:
         badTimer()
      }
   }
​
   if len(moved) > 0 {
      addAdjustedTimers(pp, moved)
   }
​
   if verifyTimers {
      verifyTimerHeap(pp)
   }
}
​
//循环的将timer 的重新添加到p 的堆里面去,并将timer 的状态改为正在等待触发
func addAdjustedTimers(pp *p, moved []*timer) {
   for _, t := range moved {
      doaddtimer(pp, t)
      if !atomic.Cas(&t.status, timerMoving, timerWaiting) {
         badTimer()
      }
   }
}

verifyTimerHeap

  • 在调试状态下,verifyTimers为将被设置为true,然后就会验证堆的顺序
func verifyTimerHeap(pp *p) {
   for i, t := range pp.timers { //遍历所有timer
      if i == 0 {
         // 堆顶timer 直接跳过
         continue
      }
	  //这个是4叉堆,所以检查parent的是否大于自己的时间,如果不是panic
      // The heap is 4-ary. See siftupTimer and siftdownTimer.
      p := (i - 1) / 4
      if t.when < pp.timers[p].when {
         print("bad timer heap at ", i, ": ", p, ": ", pp.timers[p].when, ", ", i, ": ", t.when, "\n")
         throw("bad timer heap")
      }
   }
   if numTimers := int(atomic.Load(&pp.numTimers)); len(pp.timers) != numTimers {
      println("timer heap len", len(pp.timers), "!= numTimers", numTimers)
      throw("bad timer heap len")
   }
}

checkTimers触发

深入解析go Timer 和Ticker实现原理_第6张图片

stealWork

go/src/runtime/proc.go +3015

作用是:从其他p 偷取可运行的timer 或者goroutine,这里只讨论timer

  • 这是唯一调用checkTimers 我们必须加锁处理其他p 的timers 的地方
  • timerpMask 告诉我们p 是否有timers ,如果没有的话我们就不用一直check
func stealWork(now int64) (gp *g, inheritTime bool, rnow, pollUntil int64, newWork bool){
    ................
    
    
            if stealTimersOrRunNextG && timerpMask.read(enum.position()) {
                tnow, w, ran := checkTimers(p2, now)
                now = tnow
                if w != 0 && (pollUntil == 0 || w < pollUntil) {
                    pollUntil = w
                }
                if ran {
                    
                    if gp, inheritTime := runqget(pp); gp != nil {
                        return gp, inheritTime, now, pollUntil, ranTimer
                    }
                    ranTimer = true
                }
            }
     ................
}

findrunnable

该函数作用是寻找可运行的goroutine,会触发timer

go/src/runtime/proc.go +2705

// Finds a runnable goroutine to execute.
// Tries to steal from other P's, get g from local or global queue, poll network.
func findrunnable() (gp *g, inheritTime bool) {
    ....
    now, pollUntil, _ := checkTimers(_p_, 0)
    .....
}

schedule

一种是在调度循环的时候调用 checkTimers 方法进行计时器的触发

go/src/runtime/proc.go +3291

// One round of scheduler: find a runnable goroutine and execute it.
// Never returns.
func schedule(
    .....
    top:
    pp := _g_.m.p.ptr()
    pp.preempt = false
​
    if sched.gcwaiting != 0 {
        gcstopm()
        goto top
    }
    if pp.runSafePointFn != 0 {
        runSafePointFn()
    }
​
    // Sanity check: if we are spinning, the run queue should be empty.
    // Check this before calling checkTimers, as that might call
    // goready to put a ready goroutine on the local run queue.
    if _g_.m.spinning && (pp.runnext != 0 || pp.runqhead != pp.runqtail) {
        throw("schedule: spinning with local work")
    }
​
    checkTimers(pp, 0)
    .....
) 

sysmon

Go runtime 在程序启动的时候会创建一个独立的 M 作为监控线程,叫 sysmon ,这个线程为系统级的 daemon 线程,无需 P 即可运行, sysmon 每 20us~10ms 运行一次。

每次调度器调度和窃取的时候触发,但毕竟是具有一定的随机和不确定性。系统监控触发依然是一个兜底保障,那就是runtime.sysmon 监控线程

func sysmon(){
.....
    next, _ := timeSleepUntil()
 ......   
}

timeSleepUntil

返回最近将会执行的timer,这里会遍历所有的p ,获取最先执行的timer 时间并返回

​
func timeSleepUntil() (int64, *p) {
   next := int64(maxWhen)
   var pret *p
​
   // Prevent allp slice changes. This is like retake.
   lock(&allpLock)
   for _, pp := range allp {
      if pp == nil {
         // This can happen if procresize has grown
         // allp but not yet created new Ps.
         continue
      }
​
      w := int64(atomic.Load64(&pp.timer0When))
      if w != 0 && w < next {
         next = w
         pret = pp
      }
​
      w = int64(atomic.Load64(&pp.timerModifiedEarliest))
      if w != 0 && w < next {
         next = w
         pret = pp
      }
   }
   unlock(&allpLock)
​
   return next, pret
}

4 常用使用案例

4.1超时控制

package main
​
import (
    "fmt"
    "time"
)
​
func main()  {
​
    t:=time.NewTimer(1*time.Second)
    defer t.Stop()
    go func() {
        select {
        case <-doWork():
            fmt.Println("正常退出")
            return
        case <-t.C:
            fmt.Println("超时")
            return
        }
    }()
    time.Sleep(5*time.Second)
}
func doWork()chan struct{}{
    var ch =make(chan struct{})
​
    go func() {
        time.Sleep(2*time.Second) //模拟超时
        ch<- struct{}{}
    }()
    return ch
}
​

4.2 错误导致的内存泄漏

翻车示例,在之前的项目框架里面看到过,幸好timer内存泄漏容易排查,不容易排查的话,上线出现问题可能就会被拖出去祭天了(#`O′)。下面是一个内存泄露的例子,for + Select + After 系列

package main
​
import (
   "log"
   "net/http"
   _ "net/http/pprof"
   "time"
)
​
func main() {
   go func() {
      log.Println(http.ListenAndServe("localhost:6060", nil))
   }()
   ch := make(chan int, 10)
​
   go func() {
      var i = 1
      for {
         i++
         ch <- i
      }
   }()
​
   for {
      select {
      case x := <-ch:
         println(x)
      case <-time.After(3 * time.Minute):
         println(time.Now().Unix())
      }
   }
}

输入调试命令:

如果查看分配内存可以用

go tool pprof -alloc_space  http://127.0.0.1:6060/debug/pprof/heap

查看使用内存命令

➜  awesomeProject1 go tool pprof -inuse_space  http://127.0.0.1:6060/debug/pprof/heap
Fetching profile over HTTP from http://127.0.0.1:6060/debug/pprof/heap
Saved profile in /Users/wuming/pprof/pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.008.pb.gz
Type: inuse_space
Time: Nov 7, 2021 at 1:34pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) web //图形界面
(pprof) top //查看内存占用最多项
Showing nodes accounting for 171.92MB, 99.42% of 172.92MB total
Dropped 6 nodes (cum <= 0.86MB)
Showing top 10 nodes out of 14
      flat  flat%   sum%        cum   cum%
         0     0%     0%   169.92MB 98.26%  main.main
         0     0%     0%   169.92MB 98.26%  runtime.main
         0     0%     0%   169.92MB 98.26%  time.After (inline)
  162.01MB 93.69% 93.69%   169.92MB 98.26%  time.NewTimer //这个函数占用了大部分内存
    7.91MB  4.57% 98.26%     7.91MB  4.57%  time.startTimer
       2MB  1.16% 99.42%        2MB  1.16%  runtime.allocm
         0     0% 99.42%        2MB  1.16%  runtime.mstart
         0     0% 99.42%        2MB  1.16%  runtime.mstart0
         0     0% 99.42%        2MB  1.16%  runtime.mstart1
         0     0% 99.42%        2MB  1.16%  runtime.newm
(pprof) 
​

图形界面查看示例

深入解析go Timer 和Ticker实现原理_第7张图片

原理在前面已经讲过,time.After(3 * time.Minute) 每次select都会执行一次,创建timer,但是没有调用stop 去标记被删除,timer就只有超时以后才会被标记删除,然后被gc 回收,如果超时时间比较长,再加上for 组合,就会一直创建timer,可谓是锅从天上来(#`O′)。

参考:

Windows API-SwitchToThread

Go netpoller 网络模型之源码全面解析(二)

难以驾驭的 Go timer,一文带你参透计时器的奥秘

go 专家编程

Golang 定时器底层实现深度剖析

你可能感兴趣的:(go,go官方源码,golang)