Twitter GitHub

The Go Blog 日本語訳

The Go Blogの日本語訳を公開しています。修正は https://github.com/ymotongpoo/goblog-ja/ まで。

Goの並行パターン:タイムアウトと進行(Go Concurrency Patterns: Timing out, moving on)

Goの並行パターン:タイムアウトと進行

Go Concurrency Patterns: Timing out, moving on by Andrew Gerrand

並行プログラミングにはイディオムがあります。良い例はタイムアウトです。 Goのチャンネルではタイムアウトを直接はサポートしていませんが、その実装は容易です。 たとえば、ch チャンネルから値を受信したいけれど、1秒以上は待ちたくないという状況を考えてみましょう。 まずシグナル用のチャンネルを作り、そのチャンネルに送信する前に1秒待つゴルーチンを起動します。

timeout := make(chan bool, 1)
go func() {
    time.Sleep(1 * time.Second)
    timeout <- true
}()

その後、select構文を使って chtimeout を待つようにします。 もし1秒待っても ch から何も来なければ、 timeout のケースが選択され、chからの読み込みは破棄されます。

select {
case <-ch:
    // chから読み込む
case <-timeout:
    // chからの読み込みはタイムアウト
}

timeout チャンネルは1つの値をバッファし、タイムアウトのゴルーチンがそのチャンネルに値を送り、終了できるようになっています。 このゴルーチンは、chから値が受け取られたかを知りません。(あるいは気にしていません) つまりこのゴルーチンはchからの読み込みがタイムアウトより前に起こったとしても、永遠には存在しえません。 timeout チャンネルは最終的にガベージコレクタによって回収されます。

(この例ではゴルーチンとチャンネルの機構をデモするために time.Sleep を使いました。 実際のプログラムでは time.After という、チャンネルを返し、決まった時間のあとにそのチャンネルに値を送る関数を使うべきでしょう。)

このパターンの他の例を見てみましょう。この例では複数のレプリケーションされたデータベースから同時に読み込むプログラムを扱っています。 このプログラムでは、値は1つだけ必要で最初に来た値だけを取得すべきです。

Query 関数はデータベース接続のスライスと問い合わせの文字列を引数に取ります。 この関数は各データベースに並列に問い合わせ、最初に受信した結果を返します。

func Query(conns []Conn, query string) Result {
    ch := make(chan Result, 1)
    for _, conn := range conns {
        go func(c Conn) {
            select {
            case ch <- c.DoQuery(query):
            default:
            }
        }(conn)
    }
    return <-ch
}

この例では、クロージャーがノンブロッキングに送信します。これは、 default ケース付きの select 構文内の送信操作を使うことで実現しています。 もし送信が出来なければ、直ちに default ケースが選択されます。 送信をノンブロッキングにすることで、ループ内で立ち上げられたゴルーチンが1つも無駄に生存しないことが保証されます。 しかしながら、親の関数が値を受信する前に結果が来れば、チャンネルのバッファの準備ができていないため送信は失敗する可能性があります。

この問題は競合条件として知られるものの教科書的な例ですが、修正は些細なものです。 ch チャンネルを(バッファの長さをmakeの第2引数に加えることで)バッファして、最初の送信処理が値を送れるように保証すれば良いだけです。 これによって送信処理は常に成功し、実行順に関係なく最初に到着した値が受信されるようになります。

この2つの例はGoがゴルーチン間の複雑なやりとりを表現する際の簡潔さを表しています。

By Andrew Gerrand

あわせて読みたい