初识Golang定时器

ivansli 2021/11/19 321℃ 0

1. 定时器

何为定时器?

从语义来看:定时器就是设定个指定时间,当到达了指定时间,则触发某件事件。

前端定时器
写过前端js的同学来说,为了实现某种定时触发效果,经常用到setInterval()与setTimeout()这两个方法。

其具体区别:

setInterval() 每隔指定时间就触发一次
setTimeout() 则是到了指定时间只触发一次

定时器分类:

  1. 周期性定时器(像闹钟一样,滴答、滴答、滴答......,持续不断)
  2. 一次性定时器(像炸弹一样,滴~ 蹦~,世界安静了)

2. Golang定时器

在golang提供的定时器功能中,也有这么两种常用的定时器:周期性的定时器Ticker,一次性的定时器Timer。

2.1 Timer

// 创建一次性定时器常用写法
func TheTimer() {
  // 写法1
  // func NewTimer(d Duration) *Timer
  tim := time.NewTimer()

  // 写法2
  // func After(d Duration) <-chan Time
  tim2 := time.After(time.Second)
}

2.2 Ticker

// 创建周期性定时器常用写法
func TheTicker() {
  // 写法1
  // func NewTicker(d Duration) *Ticker
  tim := time.NewTicker()

  // 写法2
  // func Tick(d Duration) <-chan Time
  tim2 := time.Tick(time.Second)
}

3. 定时器分析

func sendTime(c interface{}, seq uintptr) {
    // Non-blocking send of time on c.
    // Used in NewTimer, it cannot block anyway (buffer).
    // Used in NewTicker, dropping sends on the floor is
    // the desired behavior when the reader gets behind,
    // because the sends are periodic.
    select {
    case c.(chan Time) <- Now():
    default:
    }
}

//type Timer struct {
//    C <-chan Time
//    r runtimeTimer
//}

// 创建一次性定时器
// 源码位置 time/sleep.go
func NewTimer(d Duration) *Timer {
    c := make(chan Time, 1)
    t := &Timer{
        C: c,
        r: runtimeTimer{
            when: when(d),  // 定时器指定的触发时间点
            f:    sendTime, // 到达了指定时间,会向通道c中发送数据
            arg:  c,
        },
    }
    startTimer(&t.r)  // 添加到定时器堆
    return t
}


//type Ticker struct {
//    C <-chan Time
//    r runtimeTimer
//}

// 创建周期性定时器
// 源码位置 time/tick.go
func NewTicker(d Duration) *Ticker {
    if d <= 0 {
        panic(errors.New("non-positive interval for NewTicker"))
    }
    // Give the channel a 1-element time buffer.
    // If the client falls behind while reading, we drop ticks
    // on the floor until the client catches up.
    c := make(chan Time, 1)
    t := &Ticker{
        C: c,
        r: runtimeTimer{
            when:   when(d),  // 定时器指定的触发时间点
            period: int64(d), // 通过该字段表明这是个周期性的定时器
            f:      sendTime, // 到达了指定时间,会向通道c中发送数据
            arg:    c,
        },
    }
    startTimer(&t.r) // 添加到定时器堆
    return t
}

通过分析源码发现,不仅timer、ticker底层对应的结构体内部结构一样,而且创建定时器对象时除了runtimeTimer.period初始化不一样外,其余完全一样。

也就是说,timer、ticker使用的相同的底层结构(类型名称不一样)以及处理逻辑,并通过runtimeTimer.period字段来区分是一次性还是周期性的定时器。

// 源码位置 runtime/time.go

// 添加定时器到timer堆
// startTimer adds t to the timer heap.
//go:linkname startTimer time.startTimer
func startTimer(t *timer) {
    if raceenabled {
        racerelease(unsafe.Pointer(t))
    }
    addtimer(t) // 添加定时器
}

// addtimer adds a timer to the current P.
// This should only be called with a newly created timer.
// That avoids the risk of changing the when field of a timer in some P's heap,
// which could cause the heap to become unsorted.
func addtimer(t *timer) {
    // when must never be negative; otherwise runtimer will overflow
    // during its delta calculation and never expire other runtime timers.
    if t.when < 0 {
        t.when = maxWhen
    }
    if t.status != timerNoStatus {
        throw("addtimer called with initialized timer")
    }
    t.status = timerWaiting

    when := t.when

    pp := getg().m.p.ptr()
    lock(&pp.timersLock)
    cleantimers(pp)
    doaddtimer(pp, t) // 具体添加定时器逻辑
    unlock(&pp.timersLock)

    wakeNetPoller(when)
}

// doaddtimer adds t to the current P's heap.
// The caller must have locked the timers for pp.
func doaddtimer(pp *p, t *timer) {
    // Timers rely on the network poller, so make sure the poller
    // has started.
    if netpollInited == 0 {
        netpollGenericInit()
    }

    if t.pp != 0 {
        throw("doaddtimer: P already set in timer")
    }
    t.pp.set(pp)
    i := len(pp.timers)
    pp.timers = append(pp.timers, t) // pp.timers 为具体的定时器切片,追加当前定时器t
    siftupTimer(pp.timers, i)
    if t == pp.timers[0] {
        atomic.Store64(&pp.timer0When, uint64(t.when))
    }
    atomic.Xadd(&pp.numTimers, 1)
}

/*
//源码位置 runtime/runtime2.go
type p struct {
  ......
  // The when field of the first entry on the timer heap.
    // This is updated using atomic functions.
    // This is 0 if the timer heap is empty.
    timer0When uint64

  ......
  // Actions to take at some time. This is used to implement the
    // standard library's time package.
    // Must hold timersLock to access.
    timers []*timer  
  ......
}
*/

通过源码可以发现,定时器添加到了当前G所属的P中(Golang著名的GPM模型)。

// 源码位置 runtime/time.go

// 执行一个定时器
// runOneTimer runs a single timer.
// The caller must have locked the timers for pp.
// This will temporarily unlock the timers while running the timer function.
//go:systemstack
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 // 生成定时器对象时的 func sendTime(c interface{}, seq uintptr) 方法
    arg := t.arg // 生成定时器对象时的 通道 c
    seq := t.seq

    if t.period > 0 { // 发现是周期性定时器
        // Leave in heap but adjust next time to fire.
        delta := t.when - now
        t.when += t.period * (1 + -delta/t.period) // 下次定时器触发的时间
        siftdownTimer(pp.timers, 0)
        if !atomic.Cas(&t.status, timerRunning, timerWaiting) {
            badTimer()
        }
        updateTimer0When(pp) // 调整timer堆
    } else {
    // 发现是一次性定时器,则从timer堆中移除
        // Remove from heap.
        dodeltimer0(pp) // 从timer堆中移除
        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) // 执行 func sendTime(c interface{}, seq uintptr) 方法,即向通道c发送数据

    lock(&pp.timersLock)

    if raceenabled {
        gp := getg()
        gp.racectx = 0
    }
}

4. 定时器写法比较

// 定时器  坏的写法
// 每次都会生成新的定时器对象插入到timer堆中
func tickBad() {
    for {
        select {
        case <-time.After(time.Second):
            fmt.Println("timer")
        case <-time.Tick(time.Second):
            fmt.Println("tick")
        }
    }
}

// 定时器  好的写法
// 每次都复用同一个定时器对象
func tickGood() {
    timer := time.After(time.Second)
    ticker := time.Tick(time.Second)

    for {
        select {
        case <-timer:
            fmt.Println("timer")
        case <-ticker:
            fmt.Println("ticker")
        }
    }
}

总结

  1. 在Go中,无论是周期性定时器Ticker,还是一次性定时器Timer,其底层逻辑以及数据结构完全一致。其区别主要是通过生成定时器对象时period字段的初始化来标明是一次性还是周期性。
  2. 在定时器执行时,通过period发现是周期性定时器的话,会对该定时器下次的事件到达时间进行更新,并更新整个堆。如果是一次性定时器的话,则会从堆中移除。
  3. 值得注意的是,在定时器代码逻辑的书写中如果是写在for{} 循环当中,则需要把定时器对象写在for{}循环外面,否则每次循环都是生成新的定时器对象添加到定时器堆中,时间久了会出现意想不到的异常情况。
Golang

评论啦~