いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-119 (time.Tickとtime.NewTickerの使い分け)

概要

小ネタ。知らなかったので忘れないうちにメモメモ。

time.Tick は、特定のインターバルで処理をする場合にとても便利。

指定時間の間、特定のインターバルで処理を繰り返すようなものが書きたい場合は

time.Ticktime.After 使えばサクっとできるので好きです。

package main

import (
    "log"
    "os"
    "time"
)

func main() {
    var (
        interval = 1 * time.Second
        appLog   = log.New(os.Stdout, "[tick] ", 0)
    )

    var (
        tick     <-chan time.Time
        timeover <-chan time.Time
    )

    tick = time.Tick(interval)
    timeover = time.After(5 * time.Second)

LOOP:
    for {
        select {
        case t := <-tick:
            appLog.Println(t.UTC().Unix())
        case <-timeover:
            break LOOP
        }
    }
}

time.Ticktime.After のどちらも <-chan time.Time を返してくれます。

time.After のドキュメントをみたときに

The underlying Timer is not recovered by the garbage collector until the timer fires.

pkg.go.dev

って書いてあったので、time.Tick も同じ感じだろうって勝手に思っていました。

time.Tickは goroutine leak する

別のタイミングで、time.Tick のドキュメントみたら、以下のように書いてあってビックリ。

While Tick is useful for clients that have no need to shut down the Ticker, be aware that without a way to shut it down the underlying Ticker cannot be recovered by the garbage collector; it "leaks".

pkg.go.dev

思いっきり リーク するって書いてるやん・・。

よくよく考えたらそりゃそうだってなりました。time.After と違って、止め時がないですもんね。

ドキュメントにある通り、以下のように使い分けるのがいいみたいです。

  • インターバル処理がアプリケーションの生存期間と同じであれば time.Tick で構わない
  • 一時的な時間でインターバル処理するような場合は time.NewTicker 使って *time.Ticker 経由で処理する

サンプル

package times

import (
    "time"

    "github.com/devlights/gomy/output"
)

// TickAndTicker -- time.Tick と time.NewTicker の利用シーンの違いについてのサンプルです。
func TickAndTicker() error {
    // -------------------------------------------------
    // time.Tick と time.NewTicker の使い分け
    //
    // time.Tick は、以下の定義となっている。
    //   func Tick(d time.Duration) <-chan Time
    // 受信専用のチャネルを返しているので、内部で goroutine を
    // 起動してチャネル経由で値を返してきている。
    // 受信専用のチャネルであるので、このチャネルをユーザ側で
    // クローズすることは出来ない。なので、Tickを呼び出した際に
    // 生成される goroutine は止まることが無い。
    // 止まるタイミングがなく、ずっと動いている goroutine は
    // メインゴルーチン以外は goroutine leak していると考える。
    //
    // なので、time.Tick のドキュメントには以下のように記載されている。
    //   While Tick is useful for clients that have no need to shut down the Ticker,
    //   be aware that without a way to shut it down the underlying
    //   Ticker cannot be recovered by the garbage collector; it "leaks".
    //
    // time.Tick で生成される goroutine は終了しないので
    // アプリケーションの生存期間と同じ時間生存できるタイミングで
    // 利用する場合は便利である。
    //
    // それ以外のケース、例えば 特定の時間枠で処理する goroutineの
    // 中で利用したい場合は、time.NewTicker で明示的に time.Ticker を
    // 生成して利用するべき。time.Tickerには Stop メソッドが用意されている
    // ので、それを呼び出すと内部リソースが開放される。
    // (time.Ticker.C のチャネルはクローズされないことに注意)
    // -------------------------------------------------

    // 一時的な処理時間で動作するゴルーチン
    done := func() <-chan struct{} {
        done := make(chan struct{})
        go func() {
            defer close(done)

            // このような場合は time.Tick ではなく time.NewTicker を使うべき
            ticker := time.NewTicker(500 * time.Millisecond)
            timeout := time.After(2 * time.Second)
            defer ticker.Stop()

        LOOP:
            for {
                select {
                case t := <-ticker.C:
                    output.Stdoutl("[goroutine] ", t.UTC().Unix())
                case <-timeout:
                    break LOOP
                }
            }
        }()

        return done
    }()

    // ここはメインゴルーチン
    // ここで処理が終わるまでインターバルする場合などに利用する場合は
    // time.Tick は便利(アプリがそのまま終了するので goroutine leak は問題にならない)
    var (
        tick    <-chan time.Time
        timeout = time.After(5 * time.Second)
    )

LOOP:
    for {
        select {
        case <-done:
            // 非同期処理が終わったのでメインの出力に切り替え
            // 再びこのチャネルが select で選択されないように nil を設定しておく
            tick = time.Tick(500 * time.Millisecond)
            done = nil

            output.Stdoutl("[main     ]", "goroutine end.")
        case t := <-tick:
            output.Stdoutl("[main     ]", t.UTC().Unix())
        case <-timeout:
            break LOOP
        }
    }

    return nil
}

try-golang/time_tick_and_ticker.go at master · devlights/try-golang · GitHub

実行すると以下のようになります。

$ make run
go run github.com/devlights/try-golang/cmd/trygolang -onetime -example ""
ENTER EXAMPLE NAME: time_tick

[Name] "time_tick_and_ticker"
[goroutine]          1605243230
[goroutine]          1605243230
[goroutine]          1605243231
[goroutine]          1605243231
[main     ]          goroutine end.
[main     ]          1605243232
[main     ]          1605243232
[main     ]          1605243233
[main     ]          1605243233
[main     ]          1605243234


[Elapsed] 5.00018683s

おすすめ書籍

自分が読んだGo関連の本で、いい本って感じたものです。

Go言語による並行処理

Go言語による並行処理

スターティングGo言語 (CodeZine BOOKS)

スターティングGo言語 (CodeZine BOOKS)

  • 作者:松尾 愛賀
  • 発売日: 2016/04/15
  • メディア: 単行本(ソフトカバー)

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)


過去の記事については、以下のページからご参照下さい。

  • いろいろ備忘録日記まとめ

devlights.github.io

サンプルコードは、以下の場所で公開しています。

  • いろいろ備忘録日記サンプルソース置き場

github.com

github.com

github.com