Timer的建立
Timer是一次性的時間觸發事件,這點與Ticker不同,後者則是按一定時間間隔持續觸發時間事件。Timer常見的使用場景如下:
場景1:
t := time.AfterFunc(d, f)
場景2:
select {
case m := <-c:
handle(m)
case <-time.After(5 * time.Minute):
fmt.Println("timed out")
}
或:
t := time.NewTimer(5 * time.Minute)
select {
case m := <-c:
handle(m)
case <-t.C:
fmt.Println("timed out")
}
Timer三種建立姿勢:
t:= time.NewTimer(d)
t:= time.AfterFunc(d, f)
c:= time.After(d)
time.After跟,time.AfterFunc其中第一個After介面返回一個chan Time, 當時間到時可以讀出Timer, AfterFunc接受一個方法,當時間到時執行這個方法。
package main
import (
"time"
"fmt"
)
func main() {
a := time.After(2 * time.Second)
<- a
fmt.Println("timer receive")
time.AfterFunc(2 * time.Second, func(){
fmt.Println("timer receive")
})
}
Timer有三個要素:
* 定時時間:也就是那個d
* 觸發動作:也就是那個f
* 時間channel: 也就是t.C
內部實現
由於After跟AfterFunc差不多,這裡主要看看AfterFunc的實現
//time/sleep.go
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很簡單,就是把引數封裝為runtimeTimer,然後啟動timer(把timer新增到佇列中), 這部分程式碼在runtime/time.go中,注意這裡goFunc新啟動了一個goroutine來執行使用者的任務,這樣使用者的func就不會堵塞timer
//runtime/time.go
func startTimer(t *timer) {
if raceenabled {
racerelease(unsafe.Pointer(t))
}
addtimer(t)
}
func addtimer(t *timer) {
lock(&timers.lock)
addtimerLocked(t)
unlock(&timers.lock)
}
// Add a timer to the heap and start or kick the timer proc.
// If the new timer is earlier than any of the others.
// Timers are locked.
func addtimerLocked(t *timer) {
// when must never be negative; otherwise timerproc will overflow
// during its delta calculation and never expire other runtime·timers.
if t.when < 0 {
t.when = 1<<63 - 1
}
//新增time到全域性timer
t.i = len(timers.t)
timers.t = append(timers.t, t)
//使用最小堆演算法維護timer佇列
siftupTimer(t.i)
//如果是第一個
if t.i == 0 {
// siftup moved to top: new earliest deadline.
//如果在sleep中, 喚醒
if timers.sleeping {
timers.sleeping = false
notewakeup(&timers.waitnote)
}
//如果在排程中, 等待
if timers.rescheduling {
timers.rescheduling = false
goready(timers.gp, 0)
}
}
//如果timer還沒建立,則建立
if !timers.created {
timers.created = true
go 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]
//得到剩餘時間, 還沒到時間就sleep
delta = t.when - now
if delta > 0 {
break
}
//如果是週期性的就算下一次時間
if t.period > 0 {
// leave in heap but adjust next time to fire
t.when += t.period * (1 + -delta/t.period)
//最小堆下沉
siftdownTimer(0)
} else {
// remove from heap
//刪除將要執行的timer,(最小堆演算法)
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 // mark as removed
}
f := t.f
arg := t.arg
seq := t.seq
unlock(&timers.lock)
if raceenabled {
raceacquire(unsafe.Pointer(t))
}
//執行函式呼叫函式
f(arg, seq)
lock(&timers.lock)
}
//繼續下一個,因為可能下一個timer也到時間了
if delta < 0 || faketime > 0 {
// No timers left - put goroutine to sleep.
timers.rescheduling = true
goparkunlock(&timers.lock, "timer goroutine (idle)", traceEvGoBlock, 1)
continue
}
// At least one timer pending. Sleep until then.
timers.sleeping = true
noteclear(&timers.waitnote)
unlock(&timers.lock)
//沒到時間,睡眠delta時間
notetsleepg(&timers.waitnote, delta)
}
}
3 其他實現方法
之前看核心的timer使用的是時間輪的方式
4 API使用
func (t *Timer) Reset(d Duration) bool
Reset使t重新開始計時,(本方法返回後再)等待時間段d過去後到期。如果呼叫時t還在等待中會返回真;如果t已經到期或者被停止了會返回假。
/ if !t.Stop() {
// <-t.C
// }
// t.Reset(d)
//
// This should not be done concurrent to other receives from the Timer's
// channel.
func (t *Timer) Stop() bool
time.Timer.C
是一個 chan time.Time
而且在 Stop
時不會關閉,所以在 <-time.Timer.C
的地方如果 Stop
了就會阻塞住。
To ensure the channel is empty after a call to Stop, check the
// return value and drain the channel.
// For example, assuming the program has not received from t.C already:
//
// if !t.Stop() {
// <-t.C
// }
//
所以:
ctx, cancel := context.WithCancel(context.Background())
timer := time.NewTimer(time.Minute)
timer.Stop()
select {
// 把上面的 cancel 儲存起來可以在其它協程裡中斷阻塞的定時器
case <-ctx.Done():
// 這裡會無限阻塞
case <-timer.C:
}
按照 Timer.Stop 文件 的說法,每次呼叫 Stop 後需要判斷返回值,如果返回 false(表示 Stop 失敗,Timer 已經在 Stop 前到期)則需要排掉(drain)channel 中的事件
if !t.Stop() {
<-t.C
}
但是如果之前程式已經從 channel 中接收過事件,那麼上述 <-t.C
就會發生阻塞。可能的解決辦法是藉助 select 進行 非阻塞 排放(draining):
if !t.Stop() {
select {
case <-t.C: // try to drain the channel
default:
}
}
使用 Timer 的正確方式
參考 https://github.com/golang/go/issues/11513#issuecomment-157062583 和 https://groups.google.com/g/golang-dev/c/c9UUfASVPoU/m/tlbK2BpFEwAJ ,目前 Timer 唯一合理的使用方式是:
- 程式始終在同一個 goroutine 中進行 Timer 的 Stop、Reset 和 receive/drain channel 操作
- 程式需要維護一個狀態變數,用於記錄它是否已經從 channel 中接收過事件,進而作為 Stop 中 draining 操作的判斷依據
參考:
- 論golang Timer Reset方法正確使用