Golang定时器实现

这篇文章简单的介绍下golang time 包下定时器的实现,说道定时器,在我们开发过程中很常用,由于使用的场景不同,所以对定时器实际的实现也就不同,go的定时器并没有使用SIGALARM信号实现,而是采取最小堆的方式实现(源码包中使用数组实现的四叉树),使用这种方式定时精度很高,但是有的时候可能我们不需要这么高精度的实现,为了更高效的利用资源,有的时候也会实现一个精度比较低的算法。

跟golang定时器相关的入口主要有以下几种方法:

<-time.Tick(time.Second)
<-time.After(time.Second)
<-time.NewTicker(time.Second).C
<-time.NewTimer(time.Second).C
time.AfterFunc(time.Second, func() { /*do*/ })
time.Sleep(time.Second)

这里我们以其中NewTicker为入口,NewTicker的源码如下:

func NewTicker(d Duration) *Ticker {
    if d <= 0 {
        panic(errors.New("non-positive interval for NewTicker"))
    }
    c := make(chan Time, 1)
    t := &Ticker{
        C: c,
        r: runtimeTimer{
            // when(d)返回一个runtimeNano() + int64(d)的未来时(到期时间)
            //runtimeNano运行时当前纳秒时间
            when: when(d),
            period: int64(d), // 被唤醒的时间
            f: sendTime, // 时间到期后的回调函数
            arg: c, // 时间到期后的断言参数
        },
    }
    // 将新的定时任务添加到时间堆中
    // 编译器会将这个函数翻译为runtime.startTimer(t *runtime.timer)
    // time.runtimeTimer翻译为runtime.timer
    startTimer(&t.r)
    return t

这里有个比较重要的是startTimer(&t.r)它的实现被翻译在runtime包内

func startTimer(t *timer) {
    if raceenabled {
        racerelease(unsafe.Pointer(t))
    }
    addtimer(t)
}


func addtimer(t *timer) {
    lock(&timers.lock)
    addtimerLocked(t)
    unlock(&timers.lock)
}
上面的代码为了看着方便,我将他们都放在一起
下面代码都写出部分注释
``` go
// 使用锁将计时器添加到堆中
// 如果是第一次运行此方法则启动timerproc
func addtimerLocked(t *timer) {
    if t.when < 0 {
        t.when = 1<<63 - 1
    }
    // t.i i是定时任务数组中的索引
    // 将新的定时任务追加到定时任务数组队尾
    t.i = len(timers.t)
    timers.t = append(timers.t, t)
    // 使用数组实现的四叉树最小堆根据when(到期时间)进行排序
    siftupTimer(t.i)
    // 如果t.i 索引为0
    if t.i == 0 {
        if timers.sleeping {
            // 如果还在sleep就唤醒
            timers.sleeping = false
            // 这里基于OS的同步,并进行OS系统调用
            // 在timerproc()使goroutine从睡眠状态恢复
            notewakeup(&timers.waitnote)
        }
        if timers.rescheduling {
            timers.rescheduling = false
            // 如果没有定时器,timerproc()与goparkunlock共同sleep
            // goready这里特殊说明下,在线程创建的堆栈,它比goroutine堆栈大。
            // 函数不能增长堆栈,同时不能被调度器抢占
            goready(timers.gp, 0)
        }
    }
    if !timers.created {
        timers.created = true
        go timerproc() //这里只有初始化一次
    }
}


// Timerproc运行时间驱动的事件。
// 它sleep到计时器堆中的下一个。
// 如果addtimer插入一个新的事件,它会提前唤醒timerproc。
func timerproc() {
    timers.gp = getg()
    for {
        lock(&timers.lock)
        timers.sleeping = false
        now := nanotime()
        delta := int64(-1)
        for {
            if len(timers.t) == 0 {
                delta = -1
                break
            }
            t := timers.t[0]
            delta = t.when - now
            if delta > 0 {
                break // 时间未到
            }
            if t.period > 0 {
                // 计算下一次时间
// period被唤醒的间隔
                t.when += t.period * (1 + -delta/t.period)
                siftdownTimer(0)
            } else {
                // remove from heap
                last := len(timers.t) - 1
                if last > 0 {
                    timers.t[0] = timers.t[last]
                    timers.t[0].i = 0
                }
                timers.t[last] = nil
                timers.t = timers.t[:last]
                if last > 0 {
                    siftdownTimer(0)
                }
                t.i = -1 // 标记移除
            }
            f := t.f
            arg := t.arg
            seq := t.seq
            unlock(&timers.lock)
            if raceenabled {
                raceacquire(unsafe.Pointer(t))
            }
            f(arg, seq)
            lock(&timers.lock)
        }
        if delta < 0 || faketime > 0 {
            // 没有定时器,把goroutine sleep。
            timers.rescheduling = true
            // 将当前的goroutine放入等待状态并解锁锁。
            // goroutine也可以通过呼叫goready(gp)来重新运行。
            goparkunlock(&timers.lock, "timer goroutine (idle)", traceEvGoBlock, 1)
            continue
        }
        // At least one timer pending. Sleep until then.
        timers.sleeping = true
        timers.sleepUntil = now + delta
        // 重置
        noteclear(&timers.waitnote)
        unlock(&timers.lock)
        // 使goroutine进入睡眠状态,直到notewakeup被调用,
        // 通过notewakeup 唤醒
        notetsleepg(&timers.waitnote, delta)
    }
}

golang使用最小堆(最小堆是满足除了根节点以外的每个节点都不小于其父节点的堆)实现的定时器。golang []*timer结构如下:

golang存储定时任务结构
addtimer在堆中插入一个值,然后保持最小堆的特性,其实这个结构本质就是最小优先队列的一个应用,然后将时间转换一个绝对时间处理,通过睡眠和唤醒找出定时任务,这里阅读起来源码很容易,所以只将代码和部分注释写出。