The Go Blog
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を使用する必要があります。)
このパターンの別のバリエーションを見てみましょう。この例では、複数のレプリケートされたデータベースから同時に読み取るプログラムがあります。プログラムは答えの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ケースを持つ送信操作を使用することで実現されます。送信がすぐに通過できない場合、defaultケースが選択されます。送信を非ブロッキングにすることで、ループ内で起動されたゴルーチンのいずれもぶら下がらないことが保証されます。ただし、メイン関数が受信に到達する前に結果が到着した場合、誰も準備ができていないため、送信が失敗する可能性があります。
この問題は、競合状態として知られる典型的な例ですが、修正は些細なことです。チャネルchをバッファリングする(makeの2番目の引数としてバッファ長を追加する)だけで、最初の送信が値を配置する場所を確保できます。これにより、送信は常に成功し、実行順序に関係なく最初に到着した値が取得されます。
これら2つの例は、Goがいかに簡単にゴルーチン間の複雑な相互作用を表現できるかを示しています。