[インデックス 12395] ファイルの概要
このコミットは、Go言語の標準ライブラリの一部である expvar
パッケージにおける同期メカニズムの改善に関するものです。具体的には、expvar
パッケージが提供する変数型 (Int
, Float
, String
) の String()
メソッド(値の読み取り)と Set()
メソッド(値の書き込み)における並行処理の安全性を向上させるため、sync.Mutex
の使用を sync.RWMutex
に変更し、String
型には新たにロックを追加しています。
コミット
commit 63e383cff81a590c36be0791ebdbc5fc50b98faf
Author: David Symonds <dsymonds@golang.org>
Date: Tue Mar 6 09:13:26 2012 +1100
expvar: add locking to String, and use RWMutex properly throughout.
R=bradfitz
CC=golang-dev
https://golang.org/cl/5754043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/63e383cff81a590c36be0791ebdbc5fc50b98faf
元コミット内容
expvar: add locking to String, and use RWMutex properly throughout.
R=bradfitz
CC=golang-dev
https://golang.org/cl/5754043
変更の背景
expvar
パッケージは、Goプログラムの内部状態(変数)をHTTP経由で公開するためのメカニズムを提供します。これにより、外部の監視ツールやデバッグツールがプログラムの実行中にその状態をリアルタイムで取得できるようになります。これらの公開される変数は、複数のGoroutine(Goの軽量スレッド)から同時に読み書きされる可能性があります。
このコミット以前は、Int
および Float
型の String()
メソッド(値の読み取り)では sync.Mutex
が使用されており、String
型にはそもそもロックがありませんでした。
-
sync.Mutex
の非効率性:sync.Mutex
は排他ロックであり、一度に1つのGoroutineしかロックを保持できません。これは、読み取り操作であっても、他の読み取り操作や書き込み操作をすべてブロックすることを意味します。expvar
の変数は頻繁に読み取られる(監視される)ことが想定されるため、読み取り操作が互いにブロックし合うのは非効率的でした。特に、書き込みが稀で読み取りが頻繁なシナリオでは、この排他ロックはパフォーマンスのボトルネックとなる可能性がありました。 -
String
型の競合状態:String
型にはロックが全く実装されていなかったため、複数のGoroutineが同時にString
型の値を読み書きしようとすると、競合状態(Race Condition)が発生し、データの破損や予期せぬ動作を引き起こす可能性がありました。これは重大なバグであり、修正が必要でした。
これらの問題に対処するため、このコミットでは sync.RWMutex
の導入と、String
型への適切なロックの追加が行われました。
前提知識の解説
Go言語の sync
パッケージ
Go言語の sync
パッケージは、並行処理における同期プリミティブを提供します。
-
sync.Mutex
:- 「相互排他ロック」とも呼ばれます。
Lock()
メソッドでロックを取得し、Unlock()
メソッドでロックを解放します。- 一度に1つのGoroutineのみがロックを保持できます。
- ロックが取得されている間は、他のすべてのGoroutineは
Lock()
を呼び出すとブロックされ、ロックが解放されるまで待機します。 - 読み取りと書き込みの両方に対して排他制御が必要な場合にシンプルで効果的ですが、読み取りが頻繁に行われる場合には並行性が低下します。
-
sync.RWMutex
:- 「読み書きロック(Reader-Writer Mutex)」とも呼ばれます。
sync.Mutex
よりも柔軟なロックメカニズムを提供します。- 読み取りロック (Reader Lock):
RLock()
で取得し、RUnlock()
で解放します。複数のGoroutineが同時に読み取りロックを保持できます。つまり、複数のGoroutineが同時にデータを読み取ることができます。 - 書き込みロック (Writer Lock):
Lock()
で取得し、Unlock()
で解放します。書き込みロックは排他的であり、一度に1つのGoroutineしか書き込みロックを保持できません。書き込みロックが取得されている間は、他のすべての読み取りロックおよび書き込みロックの取得はブロックされます。 - 読み取り操作が書き込み操作よりもはるかに頻繁に行われるシナリオで、並行性を大幅に向上させることができます。
競合状態 (Race Condition)
複数のGoroutineが共有リソース(変数、データ構造など)に同時にアクセスし、少なくとも1つのGoroutineがそのリソースを変更する際に、アクセス順序によって結果が非決定的に変わってしまう状態を指します。競合状態は、プログラムのバグの一般的な原因であり、デバッグが困難な場合があります。ロックなどの同期メカニズムを使用することで、競合状態を防ぐことができます。
expvar
パッケージ
Go言語の expvar
パッケージは、実行中のGoプログラムの内部変数をHTTP経由で公開するためのシンプルなインターフェースを提供します。これにより、プログラムの稼働中にその状態を外部から監視したり、デバッグ情報を取得したりすることが容易になります。公開される変数は、Int
, Float
, String
, Map
などの型で表現されます。
技術的詳細
このコミットの主要な技術的変更点は以下の通りです。
-
Int
およびFloat
型のmu
フィールドの変更:type Int struct { i int64; mu sync.Mutex }
がtype Int struct { i int64; mu sync.RWMutex }
に変更されました。type Float struct { f float64; mu sync.Mutex }
がtype Float struct { f float64; mu sync.RWMutex }
に変更されました。- これにより、これらの型の内部状態を保護するためのロックが
sync.Mutex
からsync.RWMutex
にアップグレードされました。
-
Int
およびFloat
型のString()
メソッドの変更:v.mu.Lock()
とdefer v.mu.Unlock()
がv.mu.RLock()
とdefer v.mu.RUnlock()
に変更されました。String()
メソッドは変数の値を読み取るだけの操作であるため、書き込みロック(Lock()
)ではなく読み取りロック(RLock()
)を使用するように変更されました。これにより、複数のGoroutineが同時にInt
やFloat
の値を読み取ることが可能になり、読み取り操作の並行性が向上します。
-
String
型へのmu
フィールドの追加とロックの導入:type String struct { s string }
がtype String struct { s string; mu sync.RWMutex }
に変更されました。String
型のString()
メソッドには、v.mu.RLock()
とdefer v.mu.RUnlock()
が追加されました。これにより、String
型の値を読み取る際に読み取りロックが取得され、競合状態が防止されます。String
型のSet()
メソッドには、v.mu.Lock()
とdefer v.mu.Unlock()
が追加されました。これにより、String
型の値を書き込む際に書き込みロックが取得され、書き込み操作の排他性が保証され、競合状態が防止されます。
これらの変更により、expvar
パッケージで公開される変数の読み取り操作はより並行的に実行できるようになり、書き込み操作は引き続き排他的に実行されることでデータの整合性が保たれます。特に、String
型にロックが追加されたことで、これまで存在した競合状態の脆弱性が解消されました。
コアとなるコードの変更箇所
変更は src/pkg/expvar/expvar.go
ファイルに集中しています。
--- a/src/pkg/expvar/expvar.go
+++ b/src/pkg/expvar/expvar.go
@@ -41,12 +41,12 @@ type Var interface {
// Int is a 64-bit integer variable that satisfies the Var interface.
type Int struct {
i int64
- mu sync.Mutex
+ mu sync.RWMutex
}
func (v *Int) String() string {
-\tv.mu.Lock()\n-\tdefer v.mu.Unlock()\n+\tv.mu.RLock()\n+\tdefer v.mu.RUnlock()\n return strconv.FormatInt(v.i, 10)
}
@@ -65,12 +65,12 @@ func (v *Int) Set(value int64) {
// Float is a 64-bit float variable that satisfies the Var interface.
type Float struct {
f float64
- mu sync.Mutex
+ mu sync.RWMutex
}
func (v *Float) String() string {
-\tv.mu.Lock()\n-\tdefer v.mu.Unlock()\n+\tv.mu.RLock()\n+\tdefer v.mu.RUnlock()\n return strconv.FormatFloat(v.f, 'g', -1, 64)
}
@@ -188,12 +188,21 @@ func (v *Map) Do(f func(KeyValue)) {
// String is a string variable, and satisfies the Var interface.
type String struct {
-\ts string
+\ts string
+\tmu sync.RWMutex
}
-func (v *String) String() string { return strconv.Quote(v.s) }
+func (v *String) String() string {
+\tv.mu.RLock()\n+\tdefer v.mu.RUnlock()\n+\treturn strconv.Quote(v.s)\n+}
-func (v *String) Set(value string) { v.s = value }
+func (v *String) Set(value string) {
+\tv.mu.Lock()\n+\tdefer v.mu.Unlock()\n+\tv.s = value
+}\n
// Func implements Var by calling the function
// and formatting the returned value using JSON.
コアとなるコードの解説
Int
および Float
型の変更
-
mu sync.Mutex
からmu sync.RWMutex
への変更:Int
とFloat
の構造体定義において、排他ロック (sync.Mutex
) を読み書きロック (sync.RWMutex
) に置き換えています。これにより、これらの変数の読み取り操作と書き込み操作をより効率的に同期できるようになります。
-
String()
メソッド内のロック変更:v.mu.Lock()
とdefer v.mu.Unlock()
がv.mu.RLock()
とdefer v.mu.RUnlock()
に変更されています。String()
メソッドは変数の現在の値を文字列として返す「読み取り」操作です。sync.RWMutex
のRLock()
を使用することで、複数のGoroutineが同時にこのメソッドを呼び出して値を読み取ることが可能になります。これは、sync.Mutex
のLock()
が読み取り操作であっても他のすべての操作をブロックしていたのと対照的で、読み取りが頻繁な場合のパフォーマンスが向上します。
String
型の変更
-
mu sync.RWMutex
フィールドの追加:String
構造体にmu sync.RWMutex
フィールドが追加されました。これにより、String
型の内部状態 (s
フィールド) を保護するための同期メカニズムが導入されました。このコミット以前は、String
型にはロックが全くなく、競合状態の脆弱性がありました。
-
String()
メソッドへのロックの追加:String()
メソッドは、String
型の値を引用符で囲んだ文字列として返す「読み取り」操作です。v.mu.RLock()
とdefer v.mu.RUnlock()
が追加され、読み取りロックが適切に取得・解放されるようになりました。これにより、複数のGoroutineが同時にString
型の値を安全に読み取れるようになります。
-
Set()
メソッドへのロックの追加:Set()
メソッドは、String
型の内部値 (s
フィールド) を新しい値に更新する「書き込み」操作です。v.mu.Lock()
とdefer v.mu.Unlock()
が追加され、書き込みロックが適切に取得・解放されるようになりました。書き込みロックは排他的であるため、Set()
メソッドが実行されている間は、他の読み取り操作や書き込み操作はブロックされ、データの整合性が保証されます。
これらの変更により、expvar
パッケージの Int
, Float
, String
型は、並行環境下での安全性が大幅に向上し、特に読み取り操作の並行性が改善されました。
関連リンク
- Go言語
sync
パッケージのドキュメント: https://pkg.go.dev/sync - Go言語
expvar
パッケージのドキュメント: https://pkg.go.dev/expvar - Go言語の並行処理に関する公式ブログ記事やドキュメント (一般的な情報源として):
- Go Concurrency Patterns: https://go.dev/blog/concurrency-patterns
- Effective Go - Concurrency: https://go.dev/doc/effective_go#concurrency
参考にした情報源リンク
- Go言語の公式ドキュメント (
sync
およびexpvar
パッケージ) - Go言語のソースコード (特に
src/pkg/expvar/expvar.go
の変更履歴) - Go言語における並行処理と同期メカニズムに関する一般的な知識