time.Tick() を for ループの中に書いてはいけない

time.Tick(3 * time.Second) と書くと,3秒ごとにお知らせしてくれるチャンネルを返してくれます. 一定時間間隔で何かをチェックしたいときには便利ですよね.select と組み合わせて使います.

package main

import (
        "fmt"
        "time"
)

func main() {
        for {
                select {
                case <-time.Tick(1 * time.Second):
                        fmt.Println("fizz")
                case <-time.Tick(3 * time.Second):
                        fmt.Println("buzz")
                }
        }
}

こう書くと,多少入り乱れるとしても,次のような出力得られると期待しちゃいませんか?

期待:

fizz
fizz
fizz
buzz
fizz
fizz
fizz
buzz

でも実際に得られるのは

fizz
fizz
fizz
fizz
fizz
fizz
fizz
fizz

です.むむむ.

time.Tick() はリークする

for ループの中に time.Tick() を書いてはダメです.なぜなら,Tick() は単なる便利関数で,次のように定義されているからです.

func Tick(d Duration) <-chan Time {
    if d <= 0 {
        return nil
    }
    return NewTicker(d).C
}

コードを見ると分かるように,Ticker を毎回生成してしまいます. しかも,生成した Ticker は止める手立てがないので,リークします

ドキュメントにも書いてありました orz.

f:id:ikawaha:20151008170501p:plain

擬似的に書き直してみるとよく分かりました.

package main

import (
        "fmt"
        "time"
)

func mytick(d time.Duration) <-chan time.Time {
        fmt.Println("leak!", d)
        return time.NewTicker(d).C
}

func main() {
        for {
                select {
                case <-mytick(1 * time.Second):
                        fmt.Println("fizz")
                case <-mytick(3 * time.Second):
                        fmt.Println("buzz")
                }
        }
}

出力:

leak! 1s
leak! 3s
fizz
leak! 1s
leak! 3s
fizz
leak! 1s
leak! 3s
fizz
leak! 1s
leak! 3s
fizz
leak! 1s
leak! 3s

ひどいことになってたんですね.

すぐ終了するようなプログラムではまぁ問題ないんでしょうが, 凝ったプログラムとかライブラリで必要になるようでしたらちゃんと Ticker を自前で生成して Stop() を呼んであげましょう.

ちなみに,例で示したプログラムで期待した動作を得るにははこう書きます.

package main

import (
        "fmt"
        "time"
)

func main() {
        t1 := time.Tick(1 * time.Second)
        t3 := time.Tick(3 * time.Second)
        for {
                select {
                case <-t1.C:
                        fmt.Println("fizz")
                case <-t3.C:
                        fmt.Println("buzz")
                }
        }
}

Tick() つかったコードを見たら注意が必要ですね. とはいえ,こんなコードを書くのは僕ぐらいなものでしょうか.自戒を込めて orz.