Goアドベントカレンダーその6の穴埋め投稿です。

忙しい人のためのまとめ

  • Timer.Stop()は戻り値を見て<-timer.Cする(下記の作法参照)
  • Timer.Reset()はTimerが確実に止まってから呼ぶ
  • time.AfterFunc()は基本的には使わない
  • Timerは1つのgoroutineでしか触らない
if !timer.Stop() {
	<-timer.C
}

time.Timer型とは

Goの標準パッケージtimeにて提供されている機能で、指定時間後に一度だけ発火するイベントを作ることができます。 詳しくはドキュメントをご覧ください。 この記事の内容は、よく読めばドキュメントに書いてある内容ですが、よく読んで試さないとわかりにくかったのでまとめました。

Timerを作成する関数はNewTimerAfterFuncの2種類が提供されていて、次のような違いがあります。

timer := time.NewTimer(time.Second * 5) // 5秒後に発火するタイマー
<-timer.C // 5秒後にチャネルに通知される
timer := time.AfterFunc(time.Second * 5, func() {
	// 5秒後にこの関数が実行される
})

さらにこのTimerはStop()で途中で停止したり、Reset()で発火時間を変更することができます。

funct * TimerStop()bool

funct * TimerResetd Durationbool

一見シンプルなように見えますが、使うときはいくつか注意すべきポイントがあるので紹介します。

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()を使うべきではない