The Go Blog(Goブログ)
Goの並行処理パターン: タイムアウトと処理の継続
並行プログラミングには独自のイディオムがあります。良い例はタイムアウトです。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がゴルーチン間の複雑な相互作用を表現できるシンプルさを示しています。