[インデックス 18930] ファイルの概要
このコミットでは、Go言語の公式ドキュメントである doc/effective_go.html
と doc/go_mem.html
の2つのファイルが変更されています。
doc/effective_go.html
: Go言語を効果的に記述するためのプラクティスとイディオムを解説するドキュメントです。このコミットでは、バッファ付きチャネルをセマフォとして使用する際の誤ったイディオムが修正され、より正確な使用方法が示されています。具体的には、チャネルの初期化(プリフィル)が不要であること、およびセマフォの取得と解放の操作が修正されています。doc/go_mem.html
: Go言語のメモリモデルについて解説するドキュメントです。このコミットでは、バッファ付きチャネルにおけるセンドとレシーブの「happens before」関係に関する新しいルールが追加され、バッファ付きチャネルがセマフォとして機能する根拠が明確化されています。
コミット
commit 132e816734de8cb7d5c52ca3a5a707135fc81075
Author: Russ Cox <rsc@golang.org>
Date: Mon Mar 24 19:11:21 2014 -0400
doc: allow buffered channel as semaphore without initialization
This rule not existing has been the source of many discussions
on golang-dev and on issues. We have stated publicly that it is
true, but we have never written it down. Write it down.
Fixes #6242.
LGTM=r, dan.kortschak, iant, dvyukov
R=golang-codereviews, r, dominik.honnef, dvyukov, dan.kortschak, iant, 0xjnml
CC=golang-codereviews
https://golang.org/cl/75130045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/132e816734de8cb7d5c52ca3a5a707135fc81075
元コミット内容
doc: allow buffered channel as semaphore without initialization
This rule not existing has been the source of many discussions
on golang-dev and on issues. We have stated publicly that it is
true, but we have never written it down. Write it down.
Fixes #6242.
LGTM=r, dan.kortschak, iant, dvyukov
R=golang-codereviews, r, dominik.honnef, dvyukov, dan.kortschak, iant, 0xjnml
CC=golang-codereviews
https://golang.org/cl/75130045
変更の背景
このコミットの背景には、Go言語のコミュニティ、特に golang-dev
メーリングリストやGitHubのissueトラッカーで、バッファ付きチャネルをセマフォとして使用する際の「初期化(プリフィル)の必要性」に関する多くの議論があったことが挙げられます。
以前は、バッファ付きチャネルをセマフォとして使う場合、その容量分だけ初期値をチャネルに送信しておく(プリフィルする)のが一般的なイディオムとされていました。しかし、実際にはGoのチャネルの動作原理上、この初期化は不要であり、むしろ誤解を招く可能性がありました。Goチームは、この「初期化なしでバッファ付きチャネルをセマフォとして使用できる」というルールが事実であることを公に表明していましたが、公式ドキュメントには明記されていませんでした。
このコミットは、この重要な事実を公式ドキュメントに明文化し、混乱を解消することを目的としています。特に、effective_go.html
のセマフォの例を修正し、go_mem.html
に新しいメモリモデルのルールを追加することで、このイディオムの正当性を技術的に裏付けています。これにより、開発者がより正確かつ効率的にGoの並行処理パターンを理解し、利用できるようになります。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と並行処理の基礎知識が必要です。
-
Goのチャネル (Channels):
- Goにおけるゴルーチン間の通信と同期のための主要なプリミティブです。
- チャネルは型付けされており、特定の型の値を送受信できます。
make(chan Type)
で作成され、<-chan
で受信、chan<-
で送信します。- アンバッファードチャネル (Unbuffered Channels): 容量が0のチャネルです。送信操作は受信操作が完了するまでブロックし、受信操作は送信操作が完了するまでブロックします。これにより、送信側と受信側が同時に準備ができたときにのみ通信が行われる「同期」の役割を果たします。
- バッファードチャネル (Buffered Channels):
make(chan Type, capacity)
のように容量を指定して作成します。チャネルのバッファに指定された容量までの値を格納できます。- バッファが満杯でない限り、送信操作はブロックしません。
- バッファが空でない限り、受信操作はブロックしません。
- バッファが満杯の場合、送信操作はブロックします。
- バッファが空の場合、受信操作はブロックします。
-
セマフォ (Semaphore):
- 並行プログラミングにおける同期メカニズムの一つです。
- 複数のプロセスやスレッドが共有リソースにアクセスする数を制限するために使用されます。
- 通常、
acquire
(P操作、wait) とrelease
(V操作、signal) の2つの操作を持ちます。 acquire
操作は、利用可能なリソースがある場合にのみ続行し、リソースを消費します。利用可能なリソースがない場合はブロックします。release
操作は、リソースを解放し、待機しているプロセスやスレッドがあればそれを再開させます。- カウンティングセマフォは、同時にアクセスできるリソースの数を数えるセマフォです。
-
Goメモリモデル (Go Memory Model) と Happens Before:
- Goメモリモデルは、Goプログラムにおけるメモリ操作の順序付けを定義するものです。これにより、複数のゴルーチンが共有メモリにアクセスする際の挙動が予測可能になります。
- 「Happens Before」関係は、Goメモリモデルの核心となる概念です。これは、あるイベントが別のイベントの前に発生することを保証する順序付けのルールです。
- もしイベント
A
がイベントB
の前に発生するならば、A
の効果はB
から観測可能です。 - チャネル操作は、この「Happens Before」関係を確立する主要な手段の一つです。
- アンバッファードチャネルの場合、送信操作が完了する前に、対応する受信操作が開始されます。
- バッファードチャネルの場合、
k
番目の送信が完了する前に、k
番目の受信が開始されます。
これらの概念を理解することで、バッファ付きチャネルがどのようにしてセマフォとして機能し、なぜ初期化が不要であるのか、そしてメモリモデルの新しいルールがその正当性をどのように保証するのかが明確になります。
技術的詳細
このコミットの技術的な核心は、バッファ付きチャネルがその容量を上限とするカウンティングセマフォとして機能するというGo言語の特性を明確にし、そのための誤解を招く初期化が不要であることを示す点にあります。
従来のセマフォのイディオムでは、MaxOutstanding
の容量を持つチャネル sem
を作成した後、init()
関数などで for i := 0; i < MaxOutstanding; i++ { sem <- 1 }
のようにチャネルをプリフィル(初期化)していました。そして、セマフォの取得には <-sem
を、解放には sem <- 1
を使用していました。これは、チャネルに「トークン」を事前に置いておき、それを取り出すことでリソースを取得し、戻すことでリソースを解放するという考え方に基づいています。
しかし、Goのバッファ付きチャネルの動作を深く理解すると、このプリフィルは不要であることがわかります。
-
セマフォの取得 (Acquire):
- 新しいイディオムでは、セマフォの取得に
sem <- 1
を使用します。 - チャネル
sem
のバッファが満杯(つまり、MaxOutstanding
個のゴルーチンが既にprocess
関数を実行中)の場合、この送信操作はブロックします。 - これにより、同時に実行される
process
関数の数がMaxOutstanding
に制限されます。 - この動作は、セマフォの
acquire
操作と全く同じです。リソースが利用可能になるまで待機し、利用可能になったらそれを消費します。
- 新しいイディオムでは、セマフォの取得に
-
セマフォの解放 (Release):
- 新しいイディオムでは、セマフォの解放に
<-sem
を使用します。 process
関数が完了した後、チャネルから値を受信することで、バッファから「トークン」が一つ取り除かれ、チャネルに空きができます。- これにより、ブロックしていた他の
sem <- 1
操作が再開できるようになり、次のゴルーチンがprocess
関数を実行できるようになります。 - この動作は、セマフォの
release
操作と全く同じです。リソースを解放し、待機している他の操作を再開させます。
- 新しいイディオムでは、セマフォの解放に
この変更の正当性を裏付けるために、doc/go_mem.html
に新しいメモリモデルのルールが追加されました。
The kth send on a channel with capacity C happens before the k+Cth receive from that channel completes.
このルールは、バッファ付きチャネルにおけるセンドとレシーブの順序関係をより厳密に定義しています。具体的には、容量 C
のチャネルにおいて、k
番目の送信が完了する前に、k+C
番目の受信が完了するというものです。これは、チャネルのバッファが満杯になったときに送信がブロックし、受信によってバッファに空きができることで送信が再開されるという動作を形式的に保証します。このルールがあることで、バッファ付きチャネルをセマフォとして使用する際の「happens before」関係が明確になり、競合状態(race condition)の発生を防ぎ、プログラムの正しさを保証します。
要するに、このコミットは、Goのチャネルの設計が元々持っていたセマフォとしての能力を再認識し、その最もシンプルで効率的な使用方法を公式ドキュメントに反映させたものです。これにより、開発者は不要な初期化コードを記述することなく、より直感的にバッファ付きチャネルを並行処理の制御に活用できるようになります。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主に doc/effective_go.html
と doc/go_mem.html
の2つのファイルにわたります。
doc/effective_go.html
の変更
このファイルでは、バッファ付きチャネルをセマフォとして使用する例が修正されています。
-
セマフォの初期化部分の削除: 以前は
init()
関数内でチャネルをプリフィルするコードがありました。func init() { for i := 0; i < MaxOutstanding; i++ { sem <- 1 } }
この部分が完全に削除されました。
-
handle
関数のセマフォ操作の修正: セマフォの取得と解放の順序が逆転しています。 変更前:func handle(r *Request) { <-sem // Wait for active queue to drain. (取得) process(r) // May take a long time. sem <- 1 // Done; enable next request to run. (解放) }
変更後:
func handle(r *Request) { sem <- 1 // Wait for active queue to drain. (取得) process(r) // May take a long time. <-sem // Done; enable next request to run. (解放) }
これにより、
sem <- 1
がチャネルが満杯であればブロックする「取得」操作となり、<-sem
がチャネルから値を取り出して空きを作る「解放」操作となります。 -
Serve
関数のセマフォ操作の修正:Serve
関数内のゴルーチン起動部分でも同様の修正が行われています。 変更前:// ... for req := range queue { <-sem // 取得 go func() { process(req) sem <- 1 // 解放 }() } // ...
変更後:
// ... for req := range queue { sem <- 1 // 取得 go func() { process(req) <-sem // 解放 }() } // ...
これは、
Serve
関数内の他の2つの例(クロージャの引数渡し、シャドーイングによるreq
のコピー)でも同様に適用されています。
doc/go_mem.html
の変更
このファイルでは、Goメモリモデルに新しいルールが追加されています。
-
新しいルールの追加: バッファ付きチャネルにおける「happens before」関係を定義する以下のルールが追加されました。
<p class="rule"> The <i>k</i>th send on a channel with capacity <i>C</i> happens before the <i>k</i>+<i>C</i>th receive from that channel completes. </p>
このルールは、容量
C
のチャネルにおいて、k
番目の送信が完了する前に、k+C
番目の受信が完了することを保証します。 -
ルールの説明とセマフォへの適用: 追加されたルールの直後に、このルールがどのようにバッファ付きチャネルをカウンティングセマフォとしてモデル化できるかを説明する段落が追加されています。
<p> This rule generalizes the previous rule to buffered channels. It allows a counting semaphore to be modeled by a buffered channel: the number of items in the channel corresponds to the semaphore count, the capacity of the channel corresponds to the semaphore maximum, sending an item acquires the semaphore, and receiving an item releases the semaphore. This is a common idiom for rate-limiting work. </p>
-
セマフォの例の追加: 新しいルールを具体的に示すために、
limit
というバッファ付きチャネル(容量3)を使った並行処理のレート制限の例が追加されています。var limit = make(chan int, 3) func main() { for _, w := range work { go func() { limit <- 1 // Acquire w() <-limit // Release }() } select{} }
これらの変更は、Goの並行処理におけるバッファ付きチャネルのセマフォとしての役割をより正確に、かつ効率的に記述するための重要なドキュメントの更新です。
コアとなるコードの解説
このコミットのコアとなるコードの変更は、Goの並行処理におけるバッファ付きチャネルのセマフォとしての利用方法に関する、長年の誤解を解消し、より正確で効率的なイディオムを確立するものです。
doc/effective_go.html
の変更の解説
以前の effective_go.html
のセマフォの例では、MaxOutstanding
の容量を持つチャネル sem
を作成した後、init()
関数で MaxOutstanding
個のダミー値をチャネルに送信して「プリフィル」していました。そして、セマフォの取得には <-sem
(チャネルからの受信) を、解放には sem <- 1
(チャネルへの送信) を使用していました。
このアプローチは、チャネルに「利用可能なスロット」を表すトークンを事前に置いておき、それを取り出すことでスロットを消費し、処理が終わったらスロットを戻す、という直感的な考え方に基づいています。しかし、これはGoのバッファ付きチャネルの本来の動作を完全に活用しているわけではありませんでした。
新しいイディオムでは、init()
によるプリフィルが完全に削除されました。そして、セマフォの取得と解放の操作が逆転しました。
-
取得操作:
sem <- 1
- バッファ付きチャネルへの送信操作は、チャネルに空きがある限りブロックしません。
- しかし、チャネルのバッファが満杯(つまり、既に
MaxOutstanding
個の要素がチャネル内に存在し、それらがまだ受信されていない状態)の場合、この送信操作はブロックします。 - この「満杯であればブロックする」という性質が、まさにセマフォの
acquire
(P) 操作に相当します。同時に実行できるゴルーチンの数をチャネルの容量に制限する役割を果たします。
-
解放操作:
<-sem
- バッファ付きチャネルからの受信操作は、チャネルが空でない限りブロックしません。
- この受信操作により、チャネルから要素が一つ取り除かれ、バッファに空きができます。
- これにより、
sem <- 1
でブロックしていた他のゴルーチンが再開できるようになります。 - この「空きを作る」という性質が、セマフォの
release
(V) 操作に相当します。
この変更により、コードはより簡潔になり、チャネルの容量が直接的に並行処理の制限数として機能するという、Goのチャネルの設計思想に合致した形になりました。
doc/go_mem.html
の変更の解説
go_mem.html
に追加された新しいメモリモデルのルールは、この新しいセマフォのイディオムの正当性を形式的に保証するものです。
The kth send on a channel with capacity C happens before the k+Cth receive from that channel completes.
このルールは、バッファ付きチャネルにおける送信と受信の間の「happens before」関係を明確にします。具体的には、チャネルの容量 C
を考慮に入れた上で、k
番目の送信が完了した後に、k+C
番目の受信が完了するという順序が保証されます。
このルールが重要なのは、並行処理におけるデータ競合(data race)を防ぎ、プログラムの予測可能性を保証するためです。例えば、セマフォとして使用する場合、sem <- 1
でリソースを取得し、process(r)
を実行し、<-sem
でリソースを解放するという一連の操作が、他のゴルーチンとの間で正しく同期されることを、このメモリモデルのルールが保証します。
新しい例として追加された limit
チャネルを使ったレート制限のコードは、この新しいイディオムとメモリモデルのルールを具体的に示しています。limit <- 1
でゴルーチンが実行スロットを取得し、w()
で実際の処理を行い、<-limit
でスロットを解放するという流れは、まさにバッファ付きチャネルがカウンティングセマフォとして機能する典型的なパターンです。
総じて、このコミットは、Goのチャネルが持つ強力な同期プリミティブとしての能力を再確認し、その最も自然で効率的な利用方法を公式ドキュメントに反映させることで、Go開発者がより堅牢で理解しやすい並行プログラムを記述できるよう支援するものです。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/132e816734de8cb7d5c52ca3a5a707135fc81075
- Go CL (Code Review): https://golang.org/cl/75130045
- 関連するIssue: https://github.com/golang/go/issues/6242
参考にした情報源リンク
doc/effective_go.html
(Go言語の公式ドキュメント: Effective Go)doc/go_mem.html
(Go言語の公式ドキュメント: The Go Memory Model)- Go言語のチャネルに関する公式ドキュメントやチュートリアル
- 並行処理におけるセマフォの概念に関する一般的な情報源
- Go言語のメモリモデルと「happens before」関係に関する一般的な情報源
golang-dev
メーリングリストの議論 (具体的なリンクはコミットメッセージにはないが、背景情報として言及されている)- Go言語のGitHub Issues (特に #6242)