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

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

このコミットは、Go言語の標準ライブラリsrc/pkg/net/net.go内のネットワーク関連のコードベースに対する変更です。具体的には、Goのチャネルをベースとしたセマフォの実装方法が修正されています。

コミット

commit b3fd434ae07db5cf6385fb6b97a467e6f312c253
Author: Robert Daniel Kortschak <dan.kortschak@adelaide.edu.au>
Date:   Thu Aug 29 17:14:57 2013 +1000

    net: make channel-based semaphore depend on receive, not send
    
    R=r, dvyukov
    CC=golang-dev
    https://golang.org/cl/13348045

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

https://github.com/golang/go/commit/b3fd434ae07db5cf6385fb6b97a467e6f312c253

元コミット内容

このコミットは、src/pkg/net/net.goファイルにおいて、チャネルをセマフォとして利用するthreadLimitチャネルと、それに関連するacquireThreadおよびreleaseThread関数の実装を変更しています。

変更前は、acquireThread関数がthreadLimitチャネルへの送信操作を行い、releaseThread関数がthreadLimitチャネルからの受信操作を行っていました。

変更の背景

Go言語において、チャネルはゴルーチン間の通信だけでなく、並行処理の同期メカニズム、特にセマフォの実装にも広く利用されます。セマフォは、共有リソースへのアクセスを制御し、同時にアクセスできるゴルーチンの数を制限するために使用される同期プリミティブです。

このコミットの背景には、チャネルをセマフォとして使用する際の一般的な慣習と、より直感的で堅牢な実装への移行があります。従来のセマフォの実装では、リソースの取得(acquireThread)がチャネルへの送信(threadLimit <- struct{})で行われ、リソースの解放(releaseThread)がチャネルからの受信(<-threadLimit)で行われていました。これは、チャネルのバッファが「空きスロット」を表すという、やや非標準的なセマフォの解釈に基づいています。

しかし、より一般的なチャネルベースのセマフォのパターンでは、チャネル内の要素が「利用可能な許可(permit)」を表します。この場合、リソースの取得はチャネルからの受信(要素の消費)によって行われ、リソースの解放はチャネルへの送信(要素の追加)によって行われます。このコミットは、このより一般的で直感的なパターンに準拠するように実装を修正することを目的としています。

また、変更前はthreadLimitチャネルが初期状態で空であったため、最初のacquireThread呼び出しがブロックしないように、チャネルのバッファリングの挙動に依存していました。新しい実装では、init関数でチャネルを初期の許可数で満たすことで、セマフォの初期状態を明確にし、より予測可能な挙動を実現しています。

前提知識の解説

1. Goのゴルーチンとチャネル

  • ゴルーチン (Goroutines): Go言語における軽量な並行実行単位です。OSのスレッドよりもはるかに軽量で、数千から数百万のゴルーチンを同時に実行できます。goキーワードを使って関数呼び出しの前に置くことで簡単に起動できます。
  • チャネル (Channels): ゴルーチン間で値を安全に送受信するための通信メカニズムです。チャネルは型付けされており、特定の型の値のみを送受信できます。チャネルにはバッファリングの有無があり、バッファなしチャネルは同期通信(送信と受信が同時に行われるまでブロック)を行い、バッファありチャネルは非同期通信(バッファが満杯になるか空になるまでブロックしない)を行います。

2. セマフォ (Semaphore)

セマフォは、並行プログラミングにおける同期プリミティブの一つで、共有リソースへのアクセスを制御するために使用されます。セマフォは、内部にカウンタを持っており、このカウンタが利用可能なリソースの数を表します。

  • P操作 (Wait/Acquire): カウンタをデクリメントします。カウンタが0の場合、操作はブロックされます。
  • V操作 (Signal/Release): カウンタをインクリメントします。ブロックされているP操作があれば、それを再開させます。

Goでは、syncパッケージのsync.Mutexsync.WaitGroupなどのプリミティブがありますが、チャネルを使ってセマフォを実装することも一般的です。

3. チャネルをセマフォとして利用するパターン

チャネルをセマフォとして利用する場合、チャネルのバッファサイズがセマフォの最大許可数(同時にアクセスできるリソースの数)に対応します。

  • 許可の取得 (Acquire): チャネルから値を受信します。チャネルが空の場合(許可がない場合)、受信操作はブロックされます。
  • 許可の解放 (Release): チャネルに値を送信します。チャネルが満杯の場合(許可をこれ以上追加できない場合)、送信操作はブロックされます。

このパターンでは、チャネルに初期状態で許可の数だけダミーの値(通常はstruct{}のようなゼロサイズの型)を入れておきます。

4. Goの init 関数

init関数は、Goプログラムの実行時にmain関数が呼び出される前に自動的に実行される特別な関数です。各パッケージは複数のinit関数を持つことができ、それらは定義された順序で実行されます。init関数は、パッケージの初期化、変数の設定、または一度だけ実行する必要があるセットアップタスクに使用されます。

技術的詳細

このコミットは、src/pkg/net/net.goファイル内のthreadLimitという名前のチャネルをセマフォとして使用する方法を変更しています。

元の実装では、threadLimitは容量500のバッファ付きチャネルとして定義されていました。 var threadLimit = make(chan struct{}, 500)

そして、acquireThread関数はチャネルにstruct{}を送信し、releaseThread関数はチャネルからstruct{}を受信していました。

// 変更前
func acquireThread() {
	threadLimit <- struct{}{} // 送信で取得
}

func releaseThread() {
	<-threadLimit // 受信で解放
}

この方式では、チャネルが空の状態から始まり、acquireThreadが呼び出されるたびにチャネルに要素が追加されます。チャネルが容量に達すると、それ以上の送信はブロックされます。releaseThreadが呼び出されると、チャネルから要素が取り除かれ、空きスロットができます。これは、チャネルの「空きスロット」が利用可能なリソースを表すという、やや逆転したセマフォの概念でした。

新しい実装では、このロジックが反転され、より一般的なセマフォのパターンに準拠しています。

  1. init関数の追加: threadLimitチャネルが定義された直後に、init関数が追加されました。このinit関数は、プログラムの起動時にthreadLimitチャネルをその容量(500)いっぱいにstruct{}で満たします。これにより、チャネル内の各struct{}が利用可能な「許可」を表すようになります。

    func init() {
    	for i := 0; i < cap(threadLimit); i++ {
    		threadLimit <- struct{}{}
    	}
    }
    
  2. acquireThreadreleaseThreadのロジック反転:

    • acquireThread関数は、threadLimitチャネルからstruct{}受信するように変更されました(<-threadLimit)。チャネルが空の場合(すべての許可が消費されている場合)、この操作はブロックされます。
    • releaseThread関数は、threadLimitチャネルにstruct{}送信するように変更されました(threadLimit <- struct{})。これにより、許可がチャネルに戻され、利用可能になります。チャネルが満杯の場合(許可をこれ以上追加できない場合)、この操作はブロックされますが、これはセマフォの解放操作では通常発生しません(許可が取得された後にのみ解放されるため)。
    // 変更後
    func acquireThread() {
    	<-threadLimit // 受信で取得
    }
    
    func releaseThread() {
    	threadLimit <- struct{}{} // 送信で解放
    }
    

この変更により、threadLimitチャネルは、その中に存在するstruct{}の数が利用可能なスレッドの許可数を直接表す、より標準的なセマフォとして機能するようになりました。これにより、コードの意図がより明確になり、セマフォの概念に合致した挙動が実現されます。

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

--- a/src/pkg/net/net.go
+++ b/src/pkg/net/net.go
@@ -442,10 +442,16 @@ func (d *deadline) setTime(t time.Time) {
 
 var threadLimit = make(chan struct{}, 500)
 
+func init() {
+	for i := 0; i < cap(threadLimit); i++ {
+		threadLimit <- struct{}{}
+	}
+}
+
 func acquireThread() {
-	threadLimit <- struct{}{}
+	<-threadLimit
 }
 
 func releaseThread() {
-	<-threadLimit
+	threadLimit <- struct{}{}
 }

コアとなるコードの解説

  1. var threadLimit = make(chan struct{}, 500):

    • これは、threadLimitという名前のチャネルを宣言し、初期化しています。
    • chan struct{}は、要素として空の構造体struct{}を扱うチャネルであることを示します。struct{}はメモリを消費しないため、セマフォの許可を表すダミーの値としてよく使用されます。
    • 500はチャネルのバッファサイズです。これは、同時に500個のゴルーチンが「スレッド」を取得できることを意味し、セマフォの最大許可数となります。
  2. func init() { ... }:

    • このinit関数は、netパッケージがロードされる際に自動的に実行されます。
    • for i := 0; i < cap(threadLimit); i++ { threadLimit <- struct{}{} } ループは、threadLimitチャネルの容量(500)の数だけ、空の構造体struct{}をチャネルに送信しています。
    • この初期化により、プログラムの開始時にthreadLimitチャネルは500個の「許可」で満たされた状態になります。これにより、最初の500回のacquireThread呼び出しはブロックせずにすぐに許可を取得できます。
  3. func acquireThread() { <-threadLimit }:

    • この関数は、スレッド(またはリソース)の許可を「取得」するために呼び出されます。
    • <-threadLimitは、threadLimitチャネルから値を受信する操作です。
    • チャネルに利用可能なstruct{}がある場合、それはすぐに受信され、関数は続行します。
    • チャネルが空の場合(つまり、すべての500個の許可がすでに取得されている場合)、この受信操作は、releaseThreadが呼び出されてチャネルにstruct{}が送信されるまでブロックされます。これにより、同時に実行されるスレッドの数が500に制限されます。
  4. func releaseThread() { threadLimit <- struct{}{} }:

    • この関数は、スレッド(またはリソース)の許可を「解放」するために呼び出されます。
    • threadLimit <- struct{}は、threadLimitチャネルにstruct{}送信する操作です。
    • これにより、以前に取得された許可がチャネルに戻され、他のゴルーチンが利用できるようになります。
    • この操作は、チャネルが満杯でない限りブロックしません。セマフォの正しい使用法では、取得された許可のみが解放されるため、通常は満杯になることはありません。

この変更により、Goのnetパッケージ内のスレッド制限メカニズムが、チャネルベースのセマフォの標準的かつ直感的なパターンに準拠するようになりました。

関連リンク

参考にした情報源リンク