Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 12622] ファイルの概要

このコミットは、Go言語の公式ドキュメントに「Go Concurrency Patterns: Timing out, moving on」という新しい記事を追加するものです。この記事は、Goの並行処理パターン、特にタイムアウト処理と、複数の並行処理から最初に完了した結果を受け取るパターンについて解説しています。

コミット

  • コミットハッシュ: 5659826e43349426d86ccc8816fbf02babe19065
  • Author: Francisco Souza franciscossouza@gmail.com
  • Date: Wed Mar 14 13:03:11 2012 +1100

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/5659826e43349426d86ccc8816fbf02babe19065

元コミット内容

doc: add Go Concurrency Patterns: Timing out, moving on article

Originally published on The Go Programming Language Blog, September 23, 2010.

http://blog.golang.org/2010/09/go-concurrency-patterns-timing-out-and.html

Update #2547.

R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/5815044

変更の背景

このコミットの背景には、Go言語の並行処理モデルの強力さと柔軟性を開発者に広く理解してもらうという目的があります。特に、チャネルとselectステートメントを組み合わせることで、複雑な並行処理パターン(タイムアウトや、複数のソースからの最初の応答の取得など)をいかに簡潔かつ効率的に実装できるかを示すことが意図されています。

この記事は元々2010年9月23日にGo公式ブログで公開されたもので、Go言語の初期から重要な並行処理のイディオムとして紹介されていました。このコミットは、そのブログ記事の内容を公式ドキュメントの一部として取り込み、より永続的でアクセスしやすい形で提供することを目的としています。これにより、Go言語の学習者が並行処理のベストプラクティスを学ぶための公式なリソースが強化されます。

前提知識の解説

このコミット内容を理解するためには、以下のGo言語の基本的な並行処理の概念と関連技術についての知識が必要です。

  • Goroutine (ゴルーチン): Go言語における軽量な並行実行単位です。OSのスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行することが可能です。goキーワードを使って関数呼び出しの前に記述することで、その関数を新しいゴルーチンとして実行します。
  • Channel (チャネル): ゴルーチン間で値を安全に送受信するための通信メカニズムです。チャネルは型付けされており、特定の型の値のみを送受信できます。チャネルはデフォルトで同期的な通信(バッファなしチャネルの場合、送信と受信が同時に行われるまでブロックされる)を提供しますが、バッファ付きチャネルを作成することも可能です。
    • make(chan Type): バッファなしチャネルを作成します。
    • make(chan Type, capacity): バッファ付きチャネルを作成します。capacityはチャネルが保持できる要素の最大数です。
  • select ステートメント: 複数のチャネル操作(送信または受信)を待機し、準備ができた最初の操作を実行するためのGo言語の制御構造です。selectは、他の言語におけるswitchステートメントに似ていますが、チャネル操作に特化しています。
    • case <-ch:: chからの受信操作。
    • case ch <- value:: chへの送信操作。
    • default:: どのチャネル操作もすぐに実行できない場合に実行されるオプションのケース。defaultケースが存在する場合、selectステートメントはブロックされません。
  • time パッケージ: 時間に関連する機能を提供するGoの標準ライブラリパッケージです。
    • time.Sleep(duration): 指定された期間、現在のゴルーチンをスリープさせます。
    • time.After(duration): 指定された期間が経過した後に、現在の時刻を送信するチャネルを返します。これはタイムアウト処理でよく使われるイディオムです。
  • Race Condition (競合状態): 複数のゴルーチンが共有リソースに同時にアクセスし、少なくとも1つのゴルーチンがそのリソースを変更する可能性がある場合に発生する問題です。操作の順序によって結果が非決定的に変わる可能性があります。チャネルやsyncパッケージのミューテックスなどを使って適切に同期することで回避できます。
  • Buffered Channel (バッファ付きチャネル): チャネルに指定された数の要素をバッファとして保持できるチャネルです。バッファが満杯でない限り、送信操作はブロックされません。バッファが空でない限り、受信操作はブロックされません。タイムアウト処理や、複数のゴルーチンからの最初の応答を受け取るシナリオで、送信側が受信側を待たずに値を送信できるため、デッドロックを回避したり、ゴルーチンが不必要にブロックされるのを防ぐのに役立ちます。

技術的詳細

このコミットで追加された記事は、Go言語における2つの主要な並行処理パターンに焦点を当てています。

  1. タイムアウト処理 (Timing out):

    • Goのチャネル自体には直接的なタイムアウト機能はありませんが、selectステートメントとtimeパッケージを組み合わせることで簡単に実装できます。
    • 基本的なアイデアは、通常のチャネルからの受信操作と、一定時間後に値を送信する別のチャネル(タイムアウトチャネル)からの受信操作をselectステートメントで同時に待機することです。
    • timeout1.goの例では、time.Sleepを使ってタイムアウトチャネルに値を送信するゴルーチンを起動しています。
    • より実践的なアプローチとしては、time.After関数を使用することが推奨されます。time.Afterは指定された期間後に値を送信するチャネルを返すため、別途ゴルーチンを起動する必要がありません。
    • タイムアウトチャネルをバッファ付きにすることで、タイムアウトゴルーチンが値を送信した後に、メインのゴルーチンがタイムアウトチャネルから値を受け取らなかったとしても、タイムアウトゴルーチンがブロックされずに終了できるようになります。これにより、不要なゴルーチンのリークを防ぎます。
  2. 複数のソースからの最初の応答の取得 (Moving on):

    • このパターンは、複数の並行処理(例えば、複数のデータベースへのクエリ)を開始し、その中で最初に完了した結果のみを受け取り、残りの処理は無視する場合に有用です。
    • Query関数(timeout2.goの例)は、複数のデータベース接続に対して並行してクエリを実行し、最初に返ってきた結果を返します。
    • ここでもselectステートメントとチャネルが中心的な役割を果たします。各クエリは独自のゴルーチンで実行され、結果を共有チャネルに送信しようとします。
    • 重要なのは、結果を送信するチャネルをバッファ付きチャネルにすることです。これにより、最初の結果がチャネルに送信された際に、まだメインのゴルーチンが受信準備ができていなくても、送信操作がブロックされずに成功します。これにより、競合状態(race condition)を回避し、確実に最初の結果がチャネルに格納されることを保証します。
    • また、各ゴルーチン内の送信操作をselectステートメントのdefaultケースと組み合わせることで、非ブロッキング送信を実現しています。これにより、もしメインのゴルーチンがすでに結果を受け取ってチャネルを閉じていた場合でも、後続のゴルーチンがチャネルへの送信でブロックされることを防ぎ、ゴルーチンのリークを防ぎます。

これらのパターンは、Go言語の「Communicating Sequential Processes (CSP)」という哲学に基づいています。これは、共有メモリを介して通信するのではなく、通信するメモリを共有するという考え方です。チャネルはこの哲学を具現化するGoの主要なプリミティブであり、並行処理を安全かつ効率的に行うための強力なツールとなります。

コアとなるコードの変更箇所

このコミットでは、主に以下のファイルが変更されています。

  1. doc/Makefile:

    • 新しい記事のHTMLファイルarticles/go_concurrency_patterns_timing_out_moving_on.htmlRAWHTML変数に追加しています。これにより、ドキュメントのビルドプロセスに新しい記事が組み込まれます。
    • articles/c_go_cgo.rawhtmlも追加されていますが、これはこのコミットの主要な内容ではありません。
  2. doc/articles/go_concurrency_patterns_timing_out_moving_on.html:

    • このファイルが新規作成されています。これが「Go Concurrency Patterns: Timing out, moving on」記事の本体となるHTMLファイルです。
    • 記事のタイトル、テンプレート使用の指示、そしてGoの並行処理パターン(タイムアウトと最初の応答の取得)に関する説明、および関連するコード例への参照({{code ...}}ディレクティブ)が含まれています。
  3. doc/docs.html:

    • Goのドキュメントのインデックスページであるdocs.htmlが変更されています。
    • 既存の「Go Concurrency Patterns: Timing out, moving on」へのリンクが、外部ブログへのリンク(http://blog.golang.org/...)から、今回追加された内部ドキュメントへのリンク(/doc/articles/go_concurrency_patterns_timing_out_moving_on.html)に更新されています。これにより、記事が公式ドキュメントの一部として統合されます。
  4. doc/progs/run:

    • ドキュメント内のコード例を実行するためのスクリプトrunが変更されています。
    • 新しく追加されたコード例timeout1.gotimeout2.goall変数に追加され、ビルド対象となるように設定されています。これにより、これらのコード例がドキュメントのビルド時にコンパイルされ、正しく動作することが保証されます。
  5. doc/progs/timeout1.go:

    • このファイルが新規作成されています。タイムアウト処理の基本的な概念を示すGoのコード例です。
    • time.Sleepとバッファ付きチャネル、selectステートメントを使って、チャネルからの受信がタイムアウトするシナリオをシミュレートしています。
  6. doc/progs/timeout2.go:

    • このファイルが新規作成されています。複数の並行処理から最初に完了した結果を受け取るパターンを示すGoのコード例です。
    • 複数のデータベースクエリを並行して実行し、最初に返ってきた結果をバッファ付きチャネルと非ブロッキング送信(selectdefaultケース)を使って取得するQuery関数が定義されています。

コアとなるコードの解説

doc/progs/timeout1.go

// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package timeout

import (
	"time"
)

func Timeout() {
	ch := make(chan bool, 1) // 結果を受け取るチャネル
	timeout := make(chan bool, 1) // タイムアウトを通知するチャネル

	// タイムアウトを発生させるゴルーチン
	go func() {
		time.Sleep(1e9) // 1秒待機
		timeout <- true // タイムアウトチャネルに値を送信
	}()

	// STOP OMIT

	select {
	case <-ch:
		// chからの読み込みが発生した場合
		// ここに通常の処理を記述
	case <-timeout:
		// chからの読み込みがタイムアウトした場合
		// ここにタイムアウト時の処理を記述
	}

	// STOP OMIT
}

このコードは、Goにおけるタイムアウト処理の基本的なパターンを示しています。

  1. chというバッファ付きチャネル(容量1)が作成されます。これは、何らかの操作の結果を受け取るためのチャネルを模倣しています。
  2. timeoutという別のバッファ付きチャネル(容量1)が作成されます。これはタイムアウトを通知するために使用されます。
  3. 新しいゴルーチンが起動され、time.Sleep(1e9)(1秒)だけスリープした後、timeoutチャネルにtrueを送信します。
  4. selectステートメントが使用され、chからの受信とtimeoutからの受信のどちらかが準備できるのを待ちます。
    • もしchに先に値が送信されれば、最初のcaseが実行され、通常の処理が行われます。
    • もし1秒が経過し、timeoutチャネルに値が送信されれば、2番目のcaseが実行され、タイムアウト時の処理が行われます。
  5. timeoutチャネルがバッファ付きであるため、タイムアウトゴルーチンはtimeout <- trueを実行した後、メインのゴルーチンがその値を受け取るかどうかに関わらず、ブロックされずに終了できます。これにより、ゴルーチンのリークを防ぎます。

doc/progs/timeout2.go

// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package query

type Conn string

func (c Conn) DoQuery(query string) Result {
	// 実際のデータベースクエリ処理を模倣
	// ここでは単純に"result"を返す
	return Result("result")
}

type Result string

func Query(conns []Conn, query string) Result {
	ch := make(chan Result, 1) // 結果を受け取るバッファ付きチャネル (容量1)

	// 各接続に対して並行してクエリを実行
	for _, conn := range conns {
		go func(c Conn) {
			select {
			case ch <- c.DoQuery(query): // 結果をチャネルに送信しようとする
				// 送信が成功した場合
			default:
				// チャネルが満杯で送信できなかった場合(非ブロッキング送信)
				// このケースは、既に他のゴルーチンが結果を送信し、
				// メインのゴルーチンがその結果を受け取った後に発生する
			}
		}(conn) // connをクロージャに渡すことで、ループ変数の共有問題を避ける
	}
	return <-ch // 最初にチャネルに届いた結果を受け取る
}

// STOP OMIT

このコードは、複数の並行処理から最初に完了した結果を取得するパターンを示しています。

  1. Query関数は、Conn型のスライス(データベース接続を模倣)とクエリ文字列を受け取ります。
  2. chというバッファ付きチャネル(容量1)が作成されます。このチャネルは、最初に完了したクエリ結果を受け取るために使用されます。
  3. forループで各connに対して新しいゴルーチンが起動されます。
  4. 各ゴルーチン内で、c.DoQuery(query)が実行され、その結果をchチャネルに送信しようとします。
  5. 送信操作はselectステートメントとdefaultケースを使って非ブロッキングで行われます。
    • もしchが空で、送信が可能であれば、結果がchに送信されます。
    • もしchが既に満杯(つまり、他のゴルーチンが既に結果を送信している)であれば、defaultケースが実行され、送信はスキップされます。これにより、後から完了したゴルーチンがチャネルへの送信でブロックされるのを防ぎ、ゴルーチンのリークを防ぎます。
  6. Query関数はreturn <-chで、chから最初に届いた結果を返します。chはバッファ付き(容量1)なので、最初の送信は必ず成功し、その値がQuery関数の呼び出し元に返されます。

このパターンは、複数のレプリカデータベースにクエリを投げ、最も速い応答を採用するようなシナリオで非常に有効です。

関連リンク

参考にした情報源リンク