Goのタイマー time.Timer の作法
Goアドベントカレンダーその6の穴埋め投稿です。
忙しい人のためのまとめ
Timer.Stop()
は戻り値を見て<-timer.C
する(下記の作法参照)Timer.Reset()
はTimerが確実に止まってから呼ぶtime.AfterFunc()
は基本的には使わない- Timerは1つのgoroutineでしか触らない
if !timer.Stop() {
<-timer.C
}
time.Timer型とは
Goの標準パッケージtime
にて提供されている機能で、指定時間後に一度だけ発火するイベントを作ることができます。
詳しくはドキュメントをご覧ください。
この記事の内容は、よく読めばドキュメントに書いてある内容ですが、よく読んで試さないとわかりにくかったのでまとめました。
Timerを作成する関数はNewTimer
とAfterFunc
の2種類が提供されていて、次のような違いがあります。
timer := time.NewTimer(time.Second * 5) // 5秒後に発火するタイマー
<-timer.C // 5秒後にチャネルに通知される
timer := time.AfterFunc(time.Second * 5, func() {
// 5秒後にこの関数が実行される
})
さらにこのTimerはStop()
で途中で停止したり、Reset()
で発火時間を変更することができます。
func(t * Timer)Stop()bool
func(t * Timer)Reset(d Duration)bool
一見シンプルなように見えますが、使うときはいくつか注意すべきポイントがあるので紹介します。
Timer.Stop() の注意点
Timerは並行して動いているため、Stop()
を呼び出そうとしている間にイベントが発火してしまう可能性があります。
timer := time.NewTimer(time.Second)
time.Sleep(time.Second)
timer.Stop() // Stopしたがこの時点ですでに発火している可能性がある
select {
case <-timer.C: // 発火していた場合、すでにチャネルに通知が投げ込まれているので、こちらが動く
fmt.Println("timer")
default:
}
Stop()
呼び出し時にすでに発火していたかどうかは、戻り値で知ることができます。
後続する処理やTimerを再利用する場合に備えて、チャネルからイベントを取り除くのがおすすめです。
timer := time.NewTimer(time.Second)
time.Sleep(time.Second)
if !timer.Stop() {
<-timer.C // イベントを取り除いておく
}
select {
case <-timer.C: // チャネルは空なので動かない
fmt.Println("timer")
default:
}
ただし、他のgoroutineでもチャネルC
を待っている場合、この取り出し処理が無限に待たされる可能性が出てきてしまいます。
そもそもtimerを複数goroutineで待つのは、安全なStop()
ができなくなるのでやめたほうが良いです。
ここでのポイント
Stop()
したら戻り値を調べてチャネルC
を空にしたほうが良い- チャネル
C
を待つのは1つのgoroutineだけにして、Stop()
も同じgoroutineだけで行うべき
Timer.Reset()の注意点
Reset()
はTimerの発火時間を変更することができますが、ドキュメントにも書いてあるように、停止または発火済みのTimerでしか呼んではいけません。
停止していないTimerでReset()
を呼んだ場合、チャネルC
への通知が変更前のものか変更後のものか判断できないからです。
動いているTimerの発火時間を変更したい場合は、先にStop()
してチャネルをクリアした後でReset()
を呼ぶ必要があります。
(Reset()
にもStop()
と同じ戻り値がありますが、これは後方互換のために残されているだけです)
timer := time.NewTimer(time.Second)
time.Sleep(time.Second)
if !timer.Stop() { // 先にStopしてチャネルへの書き込みを止める
<- timer.C // 競合していない状態でチャネルを空にする
}
timer.Reset(time.Second * 10) // 停止しているので安全にReset
<-timer.C
fmt.Println("timer")
また、ループ中でTimerを再利用する場合、チャネルC
からイベントを受け取った後のStop()
の戻り値もfalse
なので注意が必要です。
無駄にチャネルC
から取り出そうとするとハングします。
d := time.Second
timer := time.NewTimer(d)
for {
select {
case <-hoge: // なにかを待つ
if !timer.Stop() { // ここでtimerを止めて必要ならチャネルをクリア
<-timer.C
}
// なにかを待っての処理
case <-timer.C:
// タイムアウトの処理
// timer.Cはすでに空なので取り出してはいけない
}
timer.Reset(d) // ここではtimerは停止または発火済みでチャネルも空
}
ここでのポイント
- Timerを確実に停止させてから
Reset()
を呼ぶ Reset()
する前にチャネルC
を空にしなければならない- ハングしないよう気をつけて空にする
Reset()
の戻り値は使ってはいけない
time.AfterFunc()の注意点
ドキュメントにもあるとおり、AfterFunc(d, f)
に設定した関数f
は、別のgoroutineで動作します。
このため、Stop()
を呼んだときにすでに動き始めている可能性が常あります。
timer := time.AfterFunc(time.Second, func() {
// 関数f
})
select {
case <-hoge: // 何かを待つ
timer.Stop() // 関数fはすでに動いているかもしれないし動いていないかもしれない
// ...
}
別のイベントと排他的に実行したい処理であれば、NewTimer
でTimerを作成してチャネルで制御したほうが良いです。
もちろんStop()
する必要がないなら有用です。
timer := time.NewTimer(time.Second)
select {
case <-hoge: // 何か待つ
if !timer.Stop() {
<-timer.C
}
// ここではf()が実行されることはない
case <-timer.C:
f() // timerが発火しかつhogeが来ていないときのみ実行できる
}
ここでのポイント
Stop()
したい処理はtime.AfterFunc()
を使うべきではない