初识Golang定时器

1. 定时器

何为定时器?

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

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

其具体区别:

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

定时器分类:

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

2. Golang定时器

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

2.1 Timer

1
2
3
4
5
6
7
8
9
10
// 创建一次性定时器常用写法
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

1
2
3
4
5
6
7
8
9
10
// 创建周期性定时器常用写法
func TheTicker() {
// 写法1
// func NewTicker(d Duration) *Ticker
tim := time.NewTicker()

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

3. 定时器分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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字段来区分是一次性还是周期性的定时器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// 源码位置 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模型)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 源码位置 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. 定时器写法比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 定时器  坏的写法
// 每次都会生成新的定时器对象插入到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{}循环外面,否则每次循环都是生成新的定时器对象添加到定时器堆中,时间久了会出现意想不到的异常情况。