[インデックス 1290] ファイルの概要
このコミットは、Go言語の標準ライブラリであるreflect
パッケージにおけるロック機構の変更に関するものです。具体的には、ミューテックスとして利用されていたバッファ付きチャネルをsync.Mutex
に置き換えることで、よりGoらしい(idiomatic)かつ効率的な排他制御を実現しています。
コミット
commit 73120ee81d2f4755bcbf03ea6b4c127afc141047
Author: Rob Pike <r@golang.org>
Date: Fri Dec 5 15:18:07 2008 -0800
use sync.Mutex instead of a channel for locking
R=rsc
DELTA=12 (3 added, 1 deleted, 8 changed)
OCL=20631
CL=20634
---
src/lib/Makefile | 2 +--
src/lib/reflect/type.go | 9 +++++----
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/src/lib/Makefile b/src/lib/Makefile
index b920aa6198..197e535d71 100644
--- a/src/lib/Makefile
+++ b/src/lib/Makefile
@@ -97,7 +97,7 @@ io.dirinstall: os.dirinstall syscall.dirinstall
net.dirinstall: once.install os.dirinstall strconv.dirinstall
os.dirinstall: syscall.dirinstall
regexp.dirinstall: os.dirinstall
-reflect.dirinstall: strconv.dirinstall
+reflect.dirinstall: strconv.dirinstall sync.dirinstall
strconv.dirinstall: os.dirinstall utf8.install
tabwriter.dirinstall: os.dirinstall io.dirinstall container/array.dirinstall
time.dirinstall: once.install os.dirinstall
diff --git a/src/lib/reflect/type.go b/src/lib/reflect/type.go
index 279f6f3150..5258cf5b5a 100644
--- a/src/lib/reflect/type.go
+++ b/src/lib/reflect/type.go
@@ -7,6 +7,8 @@
package reflect
+import "sync"
+
export type Type interface
export func ExpandType(name string) Type
@@ -390,21 +392,20 @@ var MissingStub *StubType;
var DotDotDotStub *StubType;
// The database stored in the maps is global; use locking to guarantee safety.
-var lockchan *chan bool // Channel with buffer of 1, used as a mutex
+var typestringlock sync.Mutex
func Lock() {
- lockchan <- true // block if buffer is full
+ typestringlock.Lock()
}
func Unlock() {
- <-lockchan // release waiters
+ typestringlock.Unlock()
}
func init() {
ptrsize = 8; // TODO: compute this
interfacesize = 2*ptrsize; // TODO: compute this
-\tlockchan = new(chan bool, 1);\t// unlocked at creation - buffer is empty
\tLock();\t// not necessary because of init ordering but be safe.
types = new(map[string] *Type);\n
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/73120ee81d2f4755bcbf03ea6b4c127afc141047
元コミット内容
このコミットは、Go言語のreflect
パッケージ内で使用されていた排他制御のメカニズムを変更するものです。以前は、バッファサイズが1のchan bool
(ブール型のチャネル)をミューテックスとして利用していました。Lock()
関数ではチャネルに値を送信し、Unlock()
関数ではチャネルから値を受信することで、排他制御を実現していました。
このコミットの目的は、このチャネルベースのロックを、Go言語の標準ライブラリであるsync
パッケージが提供するsync.Mutex
に置き換えることです。これにより、より明示的で、意図が明確なロックメカニズムが導入されます。
変更の背景
Go言語の初期開発段階では、並行処理のプリミティブとしてチャネルが非常に重視されていました。チャネルはゴルーチン間の通信だけでなく、排他制御の手段としても利用できるため、シンプルなミューテックスの代わりとしてバッファ付きチャネルが使われることもありました。
しかし、チャネルは主にゴルーチン間の「通信」を目的としたものであり、純粋な「排他制御」(ミューテックス)のためにはsync.Mutex
のような専用のプリミティブの方が適しています。sync.Mutex
は、ミューテックスとしてのセマンティクスが明確であり、パフォーマンス面でも特定のシナリオでチャネルよりも優れる場合があります。
このコミットは、Go言語の設計思想が成熟していく過程で、それぞれの並行処理プリミティブが最も適した用途で使われるべきであるという方向性を示していると考えられます。reflect
パッケージは、実行時に型情報を扱うため、その内部データ構造は複数のゴルーチンから同時にアクセスされる可能性があり、スレッドセーフティを確保するためのロックが不可欠です。この変更は、そのロックメカニズムをより標準的で効率的なものに改善することを目的としています。
前提知識の解説
Go言語の並行処理
Go言語は、並行処理を言語レベルでサポートしており、主に以下の2つのプリミティブを提供します。
- ゴルーチン (Goroutines): 軽量なスレッドのようなもので、Goランタイムによって管理されます。数千、数万のゴルーチンを同時に実行することが可能です。
- チャネル (Channels): ゴルーチン間で値を安全に送受信するための通信メカニズムです。チャネルは、Goの並行処理の哲学である「共有メモリを通信するのではなく、通信によってメモリを共有する」を体現しています。
ミューテックス (Mutex)
ミューテックス(Mutual Exclusionの略)は、複数のスレッド(またはゴルーチン)が共有リソースに同時にアクセスするのを防ぐための同期プリミティブです。ミューテックスは「ロック」と「アンロック」の操作を持ち、ある時点で1つのスレッドのみがロックを取得し、共有リソースにアクセスできます。他のスレッドはロックが解放されるまで待機します。
Go言語では、sync
パッケージがミューテックスを提供します。
sync.Mutex
: 最も基本的なミューテックスです。Lock()
メソッドでロックを取得し、Unlock()
メソッドでロックを解放します。sync.RWMutex
: 読み書きミューテックスです。複数の読み取り操作は同時に許可しますが、書き込み操作は排他的に行われます。
チャネルをミューテックスとして使う方法(旧来のGoのイディオム)
バッファサイズが1のチャネルは、ミューテックスとして機能させることができます。
- ロック:
ch <- true
のようにチャネルに値を送信します。チャネルのバッファが1なので、既に値が入っている場合はブロックされ、排他制御が実現されます。 - アンロック:
<-ch
のようにチャネルから値を受信します。これによりチャネルが空になり、他のゴルーチンがロックを取得できるようになります。
この方法は、Goの初期にはシンプルな排他制御によく使われましたが、sync.Mutex
が提供されてからは、純粋な排他制御にはsync.Mutex
を使うのが一般的かつ推奨されるイディオムとなりました。チャネルは通信に、ミューテックスは排他制御に、という役割分担が明確化されたためです。
reflect
パッケージ
reflect
パッケージは、Goプログラムが実行時に自身の構造を検査(リフレクション)できるようにする機能を提供します。これにより、変数の型、値、メソッドなどを動的に調べたり、操作したりすることが可能になります。例えば、JSONエンコーダ/デコーダやORM(Object-Relational Mapping)ライブラリなどは、reflect
パッケージを利用して汎用的な処理を実現しています。
reflect
パッケージの内部では、Goの型情報を管理するためのデータ構造が保持されており、これらのデータ構造は複数のゴルーチンから同時にアクセスされる可能性があるため、データ競合を防ぐための同期メカニズムが必要です。
技術的詳細
このコミットの技術的な核心は、Go言語における排他制御の「イディオム」の進化と、それに伴う実装の最適化です。
-
チャネルから
sync.Mutex
への移行:- 旧実装:
var lockchan *chan bool // Channel with buffer of 1, used as a mutex
lockchan <- true
でロック<-lockchan
でアンロック この方法は、チャネルのブロッキング特性を利用して排他制御を実現していました。バッファサイズが1のチャネルは、一度に1つの値しか保持できないため、2つ目の送信操作は最初の値が受信されるまでブロックされます。
- 新実装:
var typestringlock sync.Mutex
typestringlock.Lock()
でロックtypestringlock.Unlock()
でアンロックsync.Mutex
は、OSのミューテックスやスピンロックなどの低レベルな同期プリミティブを内部で利用しており、純粋な排他制御に特化しています。
- 旧実装:
-
依存関係の追加:
src/lib/Makefile
の変更:reflect.dirinstall
の依存関係にsync.dirinstall
が追加されました。これは、reflect
パッケージがsync
パッケージを使用するようになったため、ビルド時にsync
パッケージが利用可能であることを保証するための変更です。src/lib/reflect/type.go
でのimport "sync"
:sync.Mutex
を使用するために、明示的にsync
パッケージをインポートしています。
-
初期化の簡素化:
- 旧実装では、
lockchan = new(chan bool, 1)
のようにチャネルを明示的に初期化する必要がありました。 - 新実装では、
sync.Mutex
は構造体であり、そのゼロ値(sync.Mutex{}
)が既にアンロック状態のミューテックスとして機能するため、明示的な初期化コード(new(sync.Mutex)
やsync.Mutex{}
を代入するコード)は不要です。変数を宣言するだけで使用できます。これにより、init()
関数内のチャネル初期化に関する行が削除されています。
- 旧実装では、
この変更は、単に機能的な置き換えだけでなく、Go言語の並行処理におけるベストプラクティスへの準拠を意味します。チャネルは通信に、ミューテックスは排他制御に、という役割分担を明確にすることで、コードの意図がより明確になり、将来的なメンテナンス性やパフォーマンスの最適化にも寄与します。特に、sync.Mutex
はチャネルよりも低オーバーヘッドでロック操作を実行できる場合が多く、高頻度でロック/アンロックが行われるようなシナリオではパフォーマンス上の利点があります。
コアとなるコードの変更箇所
src/lib/Makefile
--- a/src/lib/Makefile
+++ b/src/lib/Makefile
@@ -97,7 +97,7 @@ io.dirinstall: os.dirinstall syscall.dirinstall
net.dirinstall: once.install os.dirinstall strconv.dirinstall
os.dirinstall: syscall.dirinstall
regexp.dirinstall: os.dirinstall
-reflect.dirinstall: strconv.dirinstall
+reflect.dirinstall: strconv.dirinstall sync.dirinstall
strconv.dirinstall: os.dirinstall utf8.install
tabwriter.dirinstall: os.dirinstall io.dirinstall container/array.dirinstall
time.dirinstall: once.install os.dirinstall
src/lib/reflect/type.go
--- a/src/lib/reflect/type.go
+++ b/src/lib/reflect/type.go
@@ -7,6 +7,8 @@
package reflect
+import "sync"
+
export type Type interface
export func ExpandType(name string) Type
@@ -390,21 +392,20 @@ var MissingStub *StubType;
var DotDotDotStub *StubType;
// The database stored in the maps is global; use locking to guarantee safety.
-var lockchan *chan bool // Channel with buffer of 1, used as a mutex
+var typestringlock sync.Mutex
func Lock() {
- lockchan <- true // block if buffer is full
+ typestringlock.Lock()
}
func Unlock() {
- <-lockchan // release waiters
+ typestringlock.Unlock()
}
func init() {
ptrsize = 8; // TODO: compute this
interfacesize = 2*ptrsize; // TODO: compute this
-\tlockchan = new(chan bool, 1);\t// unlocked at creation - buffer is empty
\tLock(); // not necessary because of init ordering but be safe.
types = new(map[string] *Type);\n
コアとなるコードの解説
src/lib/Makefile
の変更
reflect.dirinstall: strconv.dirinstall
+reflect.dirinstall: strconv.dirinstall sync.dirinstall
reflect
パッケージのビルド(dirinstall
ターゲット)が、strconv
パッケージに加えてsync
パッケージにも依存するようになったことを示しています。これは、reflect/type.go
がsync
パッケージをインポートするようになったため、ビルドシステムがその新しい依存関係を認識する必要があるからです。
src/lib/reflect/type.go
の変更
-
import "sync"
の追加:sync.Mutex
を使用するために、Goの標準ライブラリであるsync
パッケージがインポートされました。
-
ロック変数の変更:
-var lockchan *chan bool // Channel with buffer of 1, used as a mutex
- 以前は、バッファサイズ1の
bool
型チャネルへのポインタlockchan
がミューテックスとして宣言されていました。コメントにも「Channel with buffer of 1, used as a mutex」と明記されています。
- 以前は、バッファサイズ1の
+var typestringlock sync.Mutex
sync.Mutex
型の変数typestringlock
が宣言されました。この変数は、reflect
パッケージ内で型情報を管理するマップ(types
など)へのアクセスを保護するために使用されます。sync.Mutex
は構造体であり、そのゼロ値が既に有効なアンロック状態のミューテックスであるため、ポインタではなく直接値として宣言されています。
-
Lock()
関数の変更:-lockchan <- true // block if buffer is full
- チャネルに値を送信することでロックを取得していました。チャネルが既にフル(バッファに値がある)であれば、この操作はブロックされます。
+typestringlock.Lock()
sync.Mutex
のLock()
メソッドを呼び出すことでロックを取得します。これはsync.Mutex
の標準的なロック操作です。
-
Unlock()
関数の変更:-<-lockchan // release waiters
- チャネルから値を受信することでロックを解放していました。これによりチャネルが空になり、
lockchan <- true
で待機していた他のゴルーチンが進行できるようになります。
- チャネルから値を受信することでロックを解放していました。これによりチャネルが空になり、
+typestringlock.Unlock()
sync.Mutex
のUnlock()
メソッドを呼び出すことでロックを解放します。これはsync.Mutex
の標準的なアンロック操作です。
-
init()
関数の変更:-\tlockchan = new(chan bool, 1);\t// unlocked at creation - buffer is empty
- 以前は、
init()
関数内でlockchan
をバッファサイズ1の新しいチャネルとして初期化していました。
- 以前は、
- この行が削除されました。
sync.Mutex
はゼロ値が有効な状態であるため、明示的な初期化が不要になったためです。
これらの変更により、reflect
パッケージの内部ロックメカニズムは、Go言語のより現代的で推奨されるsync.Mutex
ベースの実装に移行しました。
関連リンク
- Go言語の
sync
パッケージのドキュメント: https://pkg.go.dev/sync - Go言語の
reflect
パッケージのドキュメント: https://pkg.go.dev/reflect - Go言語のチャネルに関する公式ブログ記事(例: Go Concurrency Patterns: Pipelines and Cancellationなど、チャネルの適切な使用法について言及しているもの)
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード(特に
sync
パッケージとreflect
パッケージの初期の実装) - Go言語の設計に関する議論やブログ記事(Goの並行処理プリミティブの進化に関するもの)