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

[インデックス 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つのプリミティブを提供します。

  1. ゴルーチン (Goroutines): 軽量なスレッドのようなもので、Goランタイムによって管理されます。数千、数万のゴルーチンを同時に実行することが可能です。
  2. チャネル (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言語における排他制御の「イディオム」の進化と、それに伴う実装の最適化です。

  1. チャネルから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のミューテックスやスピンロックなどの低レベルな同期プリミティブを内部で利用しており、純粋な排他制御に特化しています。
  2. 依存関係の追加:

    • src/lib/Makefileの変更: reflect.dirinstallの依存関係にsync.dirinstallが追加されました。これは、reflectパッケージがsyncパッケージを使用するようになったため、ビルド時にsyncパッケージが利用可能であることを保証するための変更です。
    • src/lib/reflect/type.goでのimport "sync": sync.Mutexを使用するために、明示的にsyncパッケージをインポートしています。
  3. 初期化の簡素化:

    • 旧実装では、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.gosyncパッケージをインポートするようになったため、ビルドシステムがその新しい依存関係を認識する必要があるからです。

src/lib/reflect/type.goの変更

  1. import "sync"の追加:

    • sync.Mutexを使用するために、Goの標準ライブラリであるsyncパッケージがインポートされました。
  2. ロック変数の変更:

    • -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」と明記されています。
    • +var typestringlock sync.Mutex
      • sync.Mutex型の変数typestringlockが宣言されました。この変数は、reflectパッケージ内で型情報を管理するマップ(typesなど)へのアクセスを保護するために使用されます。sync.Mutexは構造体であり、そのゼロ値が既に有効なアンロック状態のミューテックスであるため、ポインタではなく直接値として宣言されています。
  3. Lock()関数の変更:

    • -lockchan <- true // block if buffer is full
      • チャネルに値を送信することでロックを取得していました。チャネルが既にフル(バッファに値がある)であれば、この操作はブロックされます。
    • +typestringlock.Lock()
      • sync.MutexLock()メソッドを呼び出すことでロックを取得します。これはsync.Mutexの標準的なロック操作です。
  4. Unlock()関数の変更:

    • -<-lockchan // release waiters
      • チャネルから値を受信することでロックを解放していました。これによりチャネルが空になり、lockchan <- trueで待機していた他のゴルーチンが進行できるようになります。
    • +typestringlock.Unlock()
      • sync.MutexUnlock()メソッドを呼び出すことでロックを解放します。これはsync.Mutexの標準的なアンロック操作です。
  5. 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の並行処理プリミティブの進化に関するもの)