[インデックス 17573] ファイルの概要
このコミットは、Go言語の標準ライブラリnet
パッケージにおける、チャネルベースのセマフォの実装に関する変更を取り消すものです。具体的には、以前のコミット(CL 13348045 / 43675523c526)で行われたthreadLimit
チャネルのセマフォ操作(取得と解放)のロジックを元に戻しています。
コミット
commit bab302dea2f31e1ab04d17bc42050d0610c15793
Author: Russ Cox <rsc@golang.org>
Date: Wed Sep 11 20:29:22 2013 -0400
undo CL 13348045 / 43675523c526
There is no reason to do this, and it's more work.
««« original CL description
net: make channel-based semaphore depend on receive, not send
R=r, dvyukov
CC=golang-dev
https://golang.org/cl/13348045
»»»
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/13632047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/bab302dea2f31e1ab04d17bc42050d0610c15793
元コミット内容
このコミットが取り消している元のコミット(CL 13348045 / 43675523c526)のメッセージは以下の通りです。
net: make channel-based semaphore depend on receive, not send
この元の変更は、net
パッケージ内で使用されているチャネルベースのセマフォにおいて、リソースの取得(acquire)と解放(release)のロジックを、それまでの「送信(send)に依存する」方式から「受信(receive)に依存する」方式へと変更しようとしたものです。
変更の背景
このコミットの背景には、Goのnet
パッケージにおける並行処理の制御、特に同時に実行されるゴルーチンの数を制限するためのセマフォの実装があります。
元のコミット(CL 13348045)は、threadLimit
という名前のチャネルをセマフォとして使用する際のacquireThread
とreleaseThread
関数の実装を変更しました。通常、チャネルをセマフォとして使う場合、チャネルへの送信(ch <- struct{}
)がリソースの取得、チャネルからの受信(<-ch
)がリソースの解放に対応します。これは、チャネルのバッファサイズがリソースの最大数を示し、送信がバッファを埋め、受信がバッファを空けるという直感的なモデルに基づいています。
しかし、元のコミットは、この一般的なセマフォの慣習を逆転させようとしました。つまり、acquireThread
でチャネルから受信し、releaseThread
でチャネルへ送信するというロジックです。
今回のコミット(インデックス 17573)は、この変更を「元に戻す」ものです。コミットメッセージには「There is no reason to do this, and it's more work.」(これを行う理由はないし、余計な作業だ)と明確に述べられています。これは、元の変更がもたらすメリットがなく、むしろコードの理解を難しくしたり、不必要な複雑さを導入したりする可能性があったためと考えられます。
Goの設計哲学はシンプルさと明瞭さを重視しており、一般的な慣習から逸脱する変更は、それが明確な利点をもたらさない限り、通常は推奨されません。このコミットは、その哲学に沿って、不必要な複雑さを排除し、より慣用的で理解しやすいセマフォの実装に戻すことを目的としています。
前提知識の解説
Go言語のチャネル
Go言語のチャネルは、ゴルーチン間で値を送受信するためのパイプのようなものです。チャネルは、Goにおける並行処理の主要な同期メカニズムであり、共有メモリによる同期の問題(データ競合など)を避けるために設計されています。
- 宣言:
ch := make(chan Type, capacity)
Type
: チャネルで送受信される値の型。capacity
: チャネルのバッファサイズ。0
の場合はバッファなし(アンバッファードチャネル)、0
より大きい場合はバッファードチャネル。
- 送信:
ch <- value
- 値をチャネルに送信します。アンバッファードチャネルの場合、受信側が準備できるまで送信はブロックされます。バッファードチャネルの場合、バッファに空きがあればブロックされません。
- 受信:
value := <-ch
- チャネルから値を受信します。チャネルに値がなければ、受信はブロックされます。
チャネルをセマフォとして利用する
Goでは、バッファードチャネルをセマフォ(並行処理の数を制限するメカニズム)として利用するのが一般的なイディオムです。
- セマフォの初期化:
sem := make(chan struct{}, N)
N
は同時に実行を許可する最大ゴルーチン数(リソース数)です。struct{}
はメモリを消費しない空の構造体で、チャネルの値を送受信する際にデータ自体には意味がなく、単に「トークン」として機能することを示します。
- リソースの取得(P操作 / Wait):
sem <- struct{}
- チャネルにトークンを送信しようとします。チャネルのバッファが満杯(
N
個のゴルーチンが既にリソースを取得している状態)の場合、この送信操作はブロックされます。これにより、同時に実行されるゴルーチンの数がN
に制限されます。
- チャネルにトークンを送信しようとします。チャネルのバッファが満杯(
- リソースの解放(V操作 / Signal):
<-sem
- チャネルからトークンを受信します。これにより、バッファからトークンが1つ取り除かれ、別のゴルーチンがリソースを取得できるようになります。
このイディオムは、リソースの取得がチャネルへの「送信」に対応し、リソースの解放がチャネルからの「受信」に対応するという直感的なマッピングを持っています。
net
パッケージにおけるthreadLimit
Goのnet
パッケージは、ネットワーク操作を行うための基盤を提供します。ネットワークI/Oは、OSのスレッドを必要とすることが多く、同時に多数のネットワーク接続を処理する場合、OSスレッドの過剰な生成やコンテキストスイッチがパフォーマンスに影響を与える可能性があります。
threadLimit
チャネルは、このような状況で、特定のネットワーク操作(例えば、OSのネットワークコールを伴うもの)を実行するゴルーチンの数を制限するために使用されます。これにより、システムリソースの枯渇を防ぎ、安定したパフォーマンスを維持することを目的としています。
技術的詳細
このコミットは、src/pkg/net/net.go
ファイル内のthreadLimit
チャネルに関連するacquireThread
とreleaseThread
関数の実装を元に戻しています。
元のコミット(CL 13348045)では、これらの関数が以下のように変更されていました。
元のコミットでの変更(取り消される前の状態):
// acquireThread は、スレッドリソースを取得します。
func acquireThread() {
<-threadLimit // チャネルから受信することでリソースを取得
}
// releaseThread は、スレッドリソースを解放します。
func releaseThread() {
threadLimit <- struct{}{} // チャネルへ送信することでリソースを解放
}
// init 関数でチャネルを初期化し、バッファを埋めていた
func init() {
for i := 0; i < cap(threadLimit); i++ {
threadLimit <- struct{}{}
}
}
この実装では、init
関数でthreadLimit
チャネルのバッファをcap(threadLimit)
個の空の構造体で満たしていました。そして、acquireThread
ではチャネルから値を受信することでリソースを取得し、releaseThread
ではチャネルに値を送信することでリソースを解放していました。これは、一般的なセマフォのイディオムとは逆の操作です。
今回のコミット(インデックス 17573)は、この変更を元に戻し、acquireThread
とreleaseThread
を一般的なセマフォのイディオムに準拠させます。
今回のコミットによる変更(元に戻された後の状態):
// acquireThread は、スレッドリソースを取得します。
func acquireThread() {
threadLimit <- struct{}{} // チャネルへ送信することでリソースを取得
}
// releaseThread は、スレッドリソースを解放します。
func releaseThread() {
<-threadLimit // チャネルから受信することでリソースを解放
}
// init 関数は削除され、代わりにコメントが追加される
// Using send for acquire is fine here because we are not using this
// to protect any memory. All we care about is the number of goroutines
// making calls at a time.
この変更により、acquireThread
はチャネルへの送信操作(threadLimit <- struct{}
)となり、releaseThread
はチャネルからの受信操作(<-threadLimit
)となります。また、init
関数によるチャネルの初期化(バッファを埋める処理)は削除され、代わりにセマフォの目的を説明するコメントが追加されています。
このコメントは、「取得のための送信は、メモリを保護するためにこれを使用しているわけではないので問題ない。我々が関心があるのは、一度に呼び出しを行うゴルーチンの数だけである」と述べています。これは、threadLimit
がミューテックスのように共有メモリへのアクセスを保護するのではなく、単に並行して実行されるゴルーチンの数を制限するためのものであることを強調しています。
コアとなるコードの変更箇所
src/pkg/net/net.go
ファイルにおいて、以下の変更が行われました。
--- a/src/pkg/net/net.go
+++ b/src/pkg/net/net.go
@@ -408,16 +408,14 @@ func genericReadFrom(w io.Writer, r io.Reader) (n int64, err error) {
var threadLimit = make(chan struct{}, 500)
-func init() {
- for i := 0; i < cap(threadLimit); i++ {
- threadLimit <- struct{}{}
- }
-}
+// Using send for acquire is fine here because we are not using this
+// to protect any memory. All we care about is the number of goroutines
+// making calls at a time.
func acquireThread() {
- <-threadLimit
+ threadLimit <- struct{}{}
}
func releaseThread() {
- threadLimit <- struct{}{}
+ <-threadLimit
}
コアとなるコードの解説
var threadLimit = make(chan struct{}, 500)
これは、threadLimit
という名前のバッファードチャネルを宣言し、初期化しています。チャネルの型はstruct{}
(空の構造体)で、バッファサイズは500
です。これは、同時に最大500個のゴルーチンが「スレッドリソース」を取得できることを意味します。
削除されたinit
関数
元のコミットでは、init
関数内でthreadLimit
チャネルのバッファをcap(threadLimit)
個の空の構造体で埋めていました。これは、セマフォを「受信で取得、送信で解放」として使うための準備でした。このコミットでは、そのinit
関数が完全に削除されています。
追加されたコメント
init
関数の削除と同時に、以下のコメントが追加されました。
// Using send for acquire is fine here because we are not using this
// to protect any memory. All we care about is the number of goroutines
// making calls at a time.
このコメントは、threadLimit
チャネルがミューテックス(排他制御)のように共有メモリへのアクセスを保護する目的ではなく、単に同時に実行されるゴルーチンの数を制限する目的で使用されていることを明確にしています。そのため、一般的なセマフォのイディオム(送信で取得、受信で解放)を使用しても問題ないという説明です。
func acquireThread()
この関数は、スレッドリソースを取得するために呼び出されます。
- 変更前(元のコミットによる状態):
<-threadLimit
- チャネルから値を受信しようとします。チャネルに値がなければブロックされます。
- 変更後(今回のコミットによる状態):
threadLimit <- struct{}
- チャネルに空の構造体を送信しようとします。チャネルのバッファが満杯(既に500個のゴルーチンがリソースを取得している状態)の場合、この送信操作はブロックされます。これにより、同時に実行されるゴルーチンの数が500に制限されます。
func releaseThread()
この関数は、取得したスレッドリソースを解放するために呼び出されます。
- 変更前(元のコミットによる状態):
threadLimit <- struct{}
- チャネルに空の構造体を送信しようとします。
- 変更後(今回のコミットによる状態):
<-threadLimit
- チャネルから値を受信しようとします。これにより、バッファからトークンが1つ取り除かれ、別のゴルーチンがリソースを取得できるようになります。
まとめ
このコミットは、threadLimit
セマフォの操作を、Goにおけるチャネルベースのセマフォの一般的な慣習(送信で取得、受信で解放)に戻すものです。これにより、コードの意図がより明確になり、不必要な複雑さが排除されました。init
関数が削除されたのは、チャネルのバッファを事前に埋める必要がなくなったためです。
関連リンク
- Go言語のチャネルに関する公式ドキュメント: https://go.dev/tour/concurrency/2
- Go言語のセマフォに関する議論(Stack Overflowなど): https://stackoverflow.com/questions/tagged/go-semaphore
参考にした情報源リンク
- Go言語のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Go言語のコードレビューシステム (Gerrit): https://go.dev/cl/ (CL 13348045 や CL 13632047 を検索することで詳細な議論が見つかる可能性があります)
- Go言語のチャネルと並行処理に関する一般的な知識
- Go言語の
net
パッケージのソースコード - Stack Overflowなどの技術Q&AサイトでのGoセマフォに関する議論
- Go言語の設計哲学に関する記事やドキュメント