The Go Blog(Goブログ)

Goの並行処理パターン: タイムアウトと処理の継続

Andrew Gerrand
2010年9月23日

並行プログラミングには独自のイディオムがあります。良い例はタイムアウトです。Goのチャネルはタイムアウトを直接サポートしていませんが、簡単に実装できます。チャネルchから受信したいが、値が到着するまで最大1秒待機したいとします。シグナリングチャネルを作成し、チャネルに送信する前にスリープするゴルーチンを起動することから始めます。

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

次に、selectステートメントを使用して、chまたはtimeoutから受信できます。1秒後にchに何も到着しない場合、タイムアウトケースが選択され、chからの読み取りの試行は中止されます。

select {
case <-ch:
    // a read from ch has occurred
case <-timeout:
    // the read from ch has timed out
}

timeoutチャネルは1つの値を格納できるスペースでバッファリングされているため、タイムアウトゴルーチンはチャネルに送信してから終了できます。ゴルーチンは、値が受信されたかどうかを知りません(または気にしません)。これは、タイムアウトに達する前にch受信が発生した場合、ゴルーチンがいつまでもハングしないことを意味します。 timeoutチャネルは最終的にガベージコレクターによって割り当て解除されます。

(この例では、time.Sleepを使用してゴルーチンとチャネルの仕組みを示しました。実際のプログラムでは、チャネルを返し、指定された期間後にそのチャネルに送信する関数である[time.After](/pkg/time/#After)を使用する必要があります。)

このパターンの別のバリエーションを見てみましょう。この例では、複数の複製されたデータベースから同時に読み取るプログラムがあります。プログラムは回答の1つだけを必要とし、最初に到着した回答を受け入れる必要があります。

関数Queryは、データベース接続のスライスとquery文字列を受け取ります。各データベースに並行してクエリを実行し、受信した最初の応答を返します。

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

この例では、クロージャは非ブロッキング送信を実行します。これは、selectステートメントでdefaultケースを使用して送信操作を使用することで実現されます。送信がすぐに実行できない場合、デフォルトのケースが選択されます。送信を非ブロッキングにすると、ループで起動されたゴルーチンのいずれもハングしないことが保証されます。ただし、メイン関数が受信に到達する前に結果が到着した場合、誰も準備ができていないため、送信は失敗する可能性があります。

この問題は、競合状態として知られているものの教科書的な例ですが、修正は簡単です。チャネルchをバッファリングする(バッファ長をmakeの2番目の引数として追加する)だけで、最初の送信に値を配置する場所があることを保証します。これにより、送信は常に成功し、実行順序に関係なく、最初に到着した値が取得されます。

これら2つの例は、Goがゴルーチン間の複雑な相互作用を表現できるシンプルさを示しています。

次の記事:実際のGoプロジェクト:SmartTwitterとweb.go
前の記事:Go Playgroundの紹介
ブログインデックス