おひとり

できる限りひとりで楽しむための情報やプログラミング情報など。

【Go】何か処理をしながらタイムアウトをチェックする

f:id:hitoridehitode:20200927232747p:plain:w150
リアルタイムな処理をしながらタイムアウトを確認

リアルタイムにデータを処理するプログラムを書いていると、しばしばタイムアウトを考慮する必要があります。
そうすると、何か処理をしながら、都度タイムアウトしているかどうかチェックする必要があります。

ここではチャンネルとcontextを使う2種類の方法を紹介します。

チャンネルを使う方法

まずはチャンネルを使う方法です。

package main

import (
    "fmt"
    "time"
)

func main() {
    // タイムアウトの場合はdoneチャンネルをクローズする。
    done := make(chan interface{})
    cnt := 0

    // 1秒後にタイムアウトにする。
    go func() {
        time.Sleep(1 * time.Second)
        close(done)
    }()

loop:
    for {
        select {
        case <-done:
            break loop
        default:
        }

        // 何かリアルタイムな処理
        cnt++
        time.Sleep(100 * time.Millisecond)
    }

    fmt.Printf("cnt: %v\n", cnt)
}

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

cnt: 10

ここでは、doneチャンネルを用意しています。
doneチャンネルがクローズした、イコール、タイムアウトした、という実装になっています。

select文にdefaultのケースを用意しています。
select文のdefaultは他のcaseのチャンネルが全てブロックしている場合に実行したい処理を書きます。
ここでは、単にselect文を抜けるために用意しています。

contextを使う方法

先ほどはチャンネルを使いました。
次は先ほどのプログラムをcontextを使ったものに書き換えてみます。

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    // contextの機能で1秒後にタイムアウトさせる。
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()
    cnt := 0

loop:
    for {
        select {
        case <-ctx.Done():
            fmt.Println(ctx.Err())
            break loop
        default:
        }

        // 何かリアルタイムな処理
        cnt++
        time.Sleep(100 * time.Millisecond)
    }

    fmt.Printf("cnt: %v\n", cnt)
}

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

context deadline exceeded
cnt: 10

今回はcontextWithTimeout()を利用してタイムアウトを実現しています。
タイムアウトはctx.Done()で検出でき、WithTimeout()でタイムアウトした場合はctx.Done()がクローズします。

そして、context deadline exceededというエラーが発生することで、Doneの原因がタイムアウトであると知ることができます。

こちらの方がチャンネルを使う場合に比べてシンプルですし、必要に応じてWithValue()などの利用、子プロセスのキャンセルもできるためより便利といえますね。

参考文献

www.amazon.co.jp