一、引子
面試官問了一道題:每秒鐘調用一次proc並保證程序不退出。
package main
func main() {
}
func proc() {
panic("ok")
}
這道題考察的知識點主要有:
- 定時執行任務
- 捕獲
panic
錯誤
這裏主要學習、瞭解 Time.Ticker
的實現,其源代碼基於 Go 1.17.9
版本,主要在 src/time/tick.go
文件中,包含了一個結構體和四個函數。
二、Time.Ticker
Ticker
是一個週期觸發定時的計時器,它會按照一個時間間隔往 channel
發送系統當前時間,而 channel
的接收者可以以固定的時間間隔從 channel
中讀取事件。
2.1 結構體
type Ticker struct {
C <-chan Time // The channel on which the ticks are delivered.
r runtimeTimer
}
//注:該結構體在src/time/sleep.go中
type runtimeTimer struct {
pp uintptr
when int64
period int64
f func(any, uintptr) // NOTE: must not be closure
arg any
seq uintptr
nextwhen int64
status uint32
}
可以看到這個結構體包含了一個只讀的通道 C
,並每隔一段時間向其傳遞"tick"。
2.2 NewTicker()
NewTicker()
主要包含兩步:
-
創建一個
Ticker
,主要包括其中的C
屬性和r
屬性。r
屬性是runtimeTimer
類型。 -
調用
startTimer
函數,啓動Ticker
。
如果 d <= 0
會 panic
。
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: when(d),
period: int64(d),
f: sendTime, // f表示一個函數調用,這裏的sendTime表示d時間到達時向Timer.C發送當前的時間
arg: c, // arg表示在調用f的時候把參數arg傳遞給f,c就是用來接受sendTime發送時間的
},
}
startTimer(&t.r)
return t
}
這裏主要關注 f
、 arg
和 startTimer(&t.r)
。
-
f
表示一個函數調用,這裏的sendTime
表示d
時間到達時,向Timer.C
發送當前的時間; -
arg
表示在調用f
的時候把參數arg
傳遞給f
,c
就是用來接受sendTime
發送時間的。
其中 f
對應的函數爲:
func sendTime(c any, seq uintptr) {
select {
case c.(chan Time) <- Now():
default:
}
}
ticker
對象構造好後,就調用了 startTimer
函數,startTimer
具體的函數定義在 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)
}
裏面實際調用了 addtimer()
函數。
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
// Disable preemption while using pp to avoid changing another P's heap.
// 禁用p被搶佔去避免去改變其他p的堆棧
mp := acquirem()
pp := getg().m.p.ptr() // 獲取當前p
lock(&pp.timersLock)
cleantimers(pp) // 清除timers
doaddtimer(pp, t) // 添加timer到當前p的堆上,在鎖中執行
unlock(&pp.timersLock)
wakeNetPoller(when) // 添加到Netpoller
releasem(mp)
}
addtimer
就是將 timer
加到當前執行 p
的 timers
數組裏面去,調用 wakeNetPoller
方法喚醒網絡輪詢器中休眠的線程,檢查計時器被喚醒的時間(when)是否在當前輪詢預期運行的時間內,若是,就喚醒。
2.3 stop()
Stop
關閉一個 Ticker
,但不會關閉通道 t.C
,防止讀取通道發生錯誤。
func (t *Ticker) Stop() {
stopTimer(&t.r)
}
stopTimer
具體的函數定義也是在 runtime/time.go 中,實際調用了 deltimer()
函數。
func stopTimer(t *timer) bool {
return deltimer(t)
}
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) {
// Must fetch t.pp before changing status,
// as cleantimers in another goroutine
// can clear t.pp of a 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
的狀態,先修改爲“已修改”,再修改爲“刪除”。
2.4 Reset()
Reset()
調用 modTimer()
修改時間,接下來的激活將在新 d
後。
func (t *Ticker) Reset(d Duration) {
if d <= 0 {
panic("non-positive interval for Ticker.Reset")
}
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)
}
2.5 Tick()
返回 ticker
的 channel
。
func Tick(d Duration) <-chan Time {
if d <= 0 {
return nil
}
return NewTicker(d).C
}
三、小結
- Go 的定時器實質是單向通道,
time.Ticker
結構體類型中有一個time.Time
類型的單向channel
。 ticker
創建完之後,不是馬上就有一個tick
,第一個tick
在 x 秒之後。time.NewTicker
定時觸發執行任務,當下一次執行到來而當前任務還沒有執行結束時,會等待當前任務執行完畢後再執行下一次任務。Stop
不會停止定時器。這是因爲Stop
會停止Timer
,停止後,Timer
不會再被髮送,但是Stop
不會關閉通道,防止讀取通道發生錯誤。如果想停止定時器,只能讓 go 程序自動結束。
回到開頭的問題,怎麼實現每秒執行一次proc並保證程序不退出,可以使用 time.Ticker
實現定時器的功能,使用 recover()
函數捕獲 panic
錯誤。參考答案如下:
package main
import (
"fmt"
"time"
)
func main() {
go func() {
t := time.NewTicker(time.Second * 1)
for {
select {
case <-t.C:
go func() {
defer func() {
if err := recover(); err != nil {
fmt.Println("recover", err)
}
}()
}()
proc()
}
}
}()
select {}
}
func proc() {
panic("ok")
}