time.Ticker で遊ぶ【Go 1.16 バージョン】

ずいぶん前に「time.Ticker で遊ぶ」と言う記事を書いたのだが,先日リリースされた Go 1.16 で signal.NotifyContext() 関数が追加された記念に,これを使った改訂版の記事を書いてみたいと思う。

前回と同じくお題は以下の通り。

  1. 一定周期ごとの処理を行う
  2. Ctrl+C 等の割り込み処理を行う

一定周期ごとの処理を行う

これは前回の記事をほぼそのまま使いまわそう。

// +build run

package main

import (
    "fmt"
    "time"
)

func ticker() {
    t := time.NewTicker(1 * time.Second) //1秒周期の ticker
    defer func() {
        fmt.Println("Stopping ticker...")
        t.Stop()
    }()

    for {
        select {
        case now := <-t.C:
            fmt.Println(now.Format(time.RFC3339))
        }
    }
}

func main() {
    ticker()
}

前回でも説明した通り, defer 構文を使って終了時に time.Ticker.Stop() 関数で周期イベントを止めようとしているが,実際には無限ループなので return まで到達しない(笑)

NotifyContext 関数で SIGNAL を捕まえる

Go では SIGINT や SIGTERM といった OS から送信される SIGNAL をイベントとして channel に送り込む仕掛けがある(ちなみに Ctrl+C は SIGINT として送られる)。 さらに Go 1.16 では SIGNAL イベントを context.Context のキャンセル・イベントとして実装できるようになった。

たとえば,こんな感じに書ける。

// +build run

package main

import (
    "context"
    "fmt"
    "os"
    "os/signal"
    "time"
)

func ticker(ctx context.Context) {
    t := time.NewTicker(1 * time.Second) //1秒周期の ticker
    defer func() {
        fmt.Println("Stopping ticker...")
        t.Stop()
    }()

    for {
        select {
        case now := <-t.C:
            fmt.Println(now.Format(time.RFC3339))
        case <-ctx.Done():
            fmt.Println("cancellation from context:", ctx.Err())
            return
        }
    }
}

func run() {
    ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
    ticker(ctx)
}

func main() {
    run()
}

context パッケージは並行処理下で使うことが多いだろう。 たとえば run() 関数をこんな感じに書き換えてみるか。

func run() {
    ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
    var wg sync.WaitGroup
    for i := 0; i < 3; i++ {
        n := i + 1
        wg.Add(1)
        go func() {
            defer wg.Done()
            ticker(ctx, n)
        }()
    }
    wg.Wait()
}

これで平行に動作している全ての ticker() に対してキャンセルを送り込むことができる。

上のコード例ではひとつの context.Context インスタンスを複数の goroutine で使いまわしているが,以下のように

func run() {
    var wg sync.WaitGroup
    for i := 0; i < 3; i++ {
        n := i + 1
        wg.Add(1)
        go func() {
            defer wg.Done()
            ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
            ticker(ctx, n)
        }()
    }
    wg.Wait()
}

goroutine ごとに context.Context インスタンスを生成してセットしても全ての ticker()Ctrl+C で問題なく止めることができた。

キャンセル・イベントの伝搬

context パッケージは,名前の通り,異なるレイヤやドメイン間でコンテキスト情報を受け渡しするためのパッケージだが,親から子にキャンセルイベントが伝搬する性質がある(逆向きには伝搬しない)。 たとえば

func run() {
    parent, _ := signal.NotifyContext(context.Background(), os.Interrupt)
    var wg sync.WaitGroup
    for i := 0; i < 3; i++ {
        n := i + 1
        wg.Add(1)
        go func() {
            defer wg.Done()
            child, _ := context.WithTimeout(parent, time.Duration(n)*5*time.Second)
            ticker(child, n)
        }()
    }
    wg.Wait()
}

などとすれば各 goroutineticker() 関数に SIGNAL イベントとタイムアウト・イベントの両方を仕込むことができる。

また

ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)

とした場合の返り値の cancel は関数値になっていて,これをキックすることでペアとなっている context.Context インスタンス(上のコードなら ctx)にキャンセル・イベントを発生させることができる。 実際の使い方として signal.NotifyContext() 関数は main goroutine に近いところで context.WithCancel() 関数と置き換えることが多いのではないだろうか。

context について詳しくは『Go 言語による並行処理』の 4.12 章が参考になる。 素敵なキャンセル・ライフを(笑)

ブックマーク

参考図書

photo
Go言語による並行処理
Katherine Cox-Buday (著), 山口 能迪 (翻訳)
オライリージャパン 2018-10-26
単行本(ソフトカバー)
4873118468 (ASIN), 9784873118468 (EAN), 4873118468 (ISBN)
評価     

Eブック版もある。感想はこちら。 Go 言語で並行処理を書くならこの本は必読書になるだろう。

reviewed by Spiegel on 2020-01-13 (powered by PA-APIv5)

photo
プログラミング言語Go
アラン・ドノバン (著), ブライアン・カーニハン (著), 柴田芳樹 (著)
丸善出版 2016-06-20 (Release 2021-05-21)
Kindle版
B094PRR7PZ (ASIN)
評価     

Kindle 版出た! 一部内容が古びてしまったが,この本は Go 言語の教科書と言ってもいいだろう。感想はこちら

reviewed by Spiegel on 2021-05-22 (powered by PA-APIv5)