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

[インデックス 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という名前のチャネルをセマフォとして使用する際のacquireThreadreleaseThread関数の実装を変更しました。通常、チャネルをセマフォとして使う場合、チャネルへの送信(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チャネルに関連するacquireThreadreleaseThread関数の実装を元に戻しています。

元のコミット(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)は、この変更を元に戻し、acquireThreadreleaseThreadを一般的なセマフォのイディオムに準拠させます。

今回のコミットによる変更(元に戻された後の状態):

// 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言語のコミット履歴 (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言語の設計哲学に関する記事やドキュメント