[インデックス 18893] ファイルの概要
コミット
commit 666f5b4a89c52901c26b992a05fd54479fd6fad9
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Tue Mar 18 11:38:39 2014 -0700
expvar: don't recursively acquire Map.RLock
Fixes #7575
LGTM=iant
R=dvyukov, iant
CC=golang-codereviews
https://golang.org/cl/77540044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/666f5b4a89c52901c26b992a05fd54479fd6fad9
元コミット内容
このコミットは、Go言語のexpvar
パッケージにおいて、Map.RLock
が再帰的に取得される問題を修正します。具体的には、Map
型のString()
メソッド内でDo
メソッドを呼び出す際に発生するデッドロックの可能性を解消します。
変更の背景
このコミットは、Go issue #7575 を修正するために行われました。この問題は、expvar.Map
のString()
メソッドが、その内部でDo()
メソッドを呼び出す際に発生する可能性のあるデッドロックに関するものです。
expvar.Map
は、プログラムの実行中に公開される変数(メトリクスなど)を管理するためのパッケージです。Map
型は、キーと値のペアを保持し、その値はexpvar.Var
インターフェースを実装する任意の型にすることができます。Map.String()
メソッドは、Map
の内容をJSON形式の文字列として表現するために使用されます。
元の実装では、Map.String()
メソッドがMap
の読み取りロック(RLock
)を取得し、そのロックを保持したままMap.Do()
メソッドを呼び出していました。Map.Do()
メソッドもまた、Map
の読み取りロックを取得しようとします。これにより、同じゴルーチン内で同じロックを再帰的に取得しようとすることになり、デッドロックが発生する可能性がありました。特に、Map.String()
が呼び出されるコンテキスト(例えば、HTTPハンドラがexpvar
の値を公開する場合など)によっては、このデッドロックがアプリケーションのハングアップを引き起こす可能性がありました。
前提知識の解説
expvar
パッケージ
expvar
パッケージは、Goプログラムの内部状態を公開するための標準ライブラリです。これにより、実行中のアプリケーションのメトリクスや統計情報を簡単に監視できます。公開された変数は、HTTP経由で/debug/vars
エンドポイントからJSON形式でアクセスできます。
sync.RWMutex
(読み書きミューテックス)
sync.RWMutex
は、Go言語で並行処理を安全に行うための同期プリミティブです。これは、読み取り操作と書き込み操作を区別してロックを管理します。
Lock()
: 書き込みロックを取得します。一度に1つのゴルーチンのみが書き込みロックを保持できます。書き込みロックが保持されている間は、他のすべての読み取りロックおよび書き込みロックの取得はブロックされます。Unlock()
: 書き込みロックを解放します。RLock()
: 読み取りロックを取得します。複数のゴルーチンが同時に読み取りロックを保持できます。ただし、書き込みロックが保持されている間は、読み取りロックの取得はブロックされます。RUnlock()
: 読み取りロックを解放します。
expvar.Map
は、内部でsync.RWMutex
を使用して、マップへの並行アクセスを保護しています。
デッドロック
デッドロックとは、複数のプロセスやスレッドが、互いに相手が保持しているリソースの解放を待っている状態になり、結果としてどのプロセスも処理を進められなくなる状態を指します。今回のケースでは、同じゴルーチンが同じsync.RWMutex
の読み取りロックを再帰的に取得しようとすることで発生する可能性がありました。sync.RWMutex
は再帰的なロック取得をサポートしていません。
技術的詳細
このコミットの核心は、expvar.Map
のString()
メソッド内でMap.Do()
メソッドを呼び出す際に発生する再帰的なロック取得の問題を解決することです。
元のコードでは、Map.String()
メソッドがv.mu.RLock()
を呼び出して読み取りロックを取得していました。その後、v.Do(func(kv KeyValue) { ... })
を呼び出していました。Map.Do()
メソッドの定義を見ると、このメソッドもまたv.mu.RLock()
を呼び出していました。
// func (v *Map) Do(f func(KeyValue)) {
// v.mu.RLock() // ここでロックを取得
// defer v.mu.RUnlock()
// // ...
// }
したがって、String()
メソッドが既にv.mu
の読み取りロックを保持している状態で、Do()
メソッドが再度v.mu
の読み取りロックを取得しようとすると、デッドロックが発生する可能性がありました。sync.RWMutex
は再帰的なロックを許可しないため、同じゴルーチンが既に保持しているロックを再度取得しようとすると、そのゴルーチン自身がブロックされ、処理が停止してしまいます。
この問題を解決するために、新しい内部ヘルパーメソッドdoLocked
が導入されました。
doLocked(f func(KeyValue))
: このメソッドは、Map
の読み取りロックが既に保持されていることを前提として、マップ内の各エントリに対して関数f
を実行します。このメソッド自体はロックを取得しません。
変更後、Map.String()
メソッドはv.Do
の代わりにv.doLocked
を呼び出すようになりました。
// func (v *Map) String() string {
// // ...
// // v.Do(func(kv KeyValue) { // 変更前
// v.doLocked(func(kv KeyValue) { // 変更後
// // ...
// }
そして、元のMap.Do()
メソッドは、外部から呼び出された際に一度だけロックを取得し、その後にdoLocked
を呼び出すように変更されました。
func (v *Map) Do(f func(KeyValue)) {
v.mu.RLock() // ここで一度だけロックを取得
defer v.mu.RUnlock()
v.doLocked(f) // ロックが保持された状態でdoLockedを呼び出す
}
この変更により、String()
メソッドがMap
の読み取りロックを保持したままdoLocked
を呼び出すことが可能になり、doLocked
はロックを再取得しようとしないため、デッドロックが回避されます。Do()
メソッドは引き続き外部からの呼び出しに対して安全なロックメカニズムを提供します。
コアとなるコードの変更箇所
src/pkg/expvar/expvar.go
ファイルが変更されました。
--- a/src/pkg/expvar/expvar.go
+++ b/src/pkg/expvar/expvar.go
@@ -108,7 +108,7 @@ func (v *Map) String() string {
var b bytes.Buffer
fmt.Fprintf(&b, "{\")
first := true
- v.Do(func(kv KeyValue) {
+ v.doLocked(func(kv KeyValue) {
if !first {
fmt.Fprintf(&b, ", ")
}
@@ -202,6 +202,12 @@ func (v *Map) AddFloat(key string, delta float64) {
func (v *Map) Do(f func(KeyValue)) {
v.mu.RLock()
defer v.mu.RUnlock()
+ v.doLocked(f)
+}
+
+// doRLocked calls f for each entry in the map.
+// v.mu must be held for reads.
+func (v *Map) doLocked(f func(KeyValue)) {
for _, k := range v.keys {
f(KeyValue{k, v.m[k]})
}
コアとなるコードの解説
-
func (v *Map) String() string
の変更:- 変更前:
v.Do(func(kv KeyValue) { ... })
を呼び出していました。 - 変更後:
v.doLocked(func(kv KeyValue) { ... })
を呼び出すように変更されました。 - これにより、
String()
メソッドが既に保持している読み取りロックを、Do()
メソッドが再帰的に取得しようとする問題を回避します。
- 変更前:
-
func (v *Map) Do(f func(KeyValue))
の変更:- このメソッドは、外部から
Map
の要素をイテレートするために使用されます。 - 変更後も、
v.mu.RLock()
とdefer v.mu.RUnlock()
を使用して、マップへの安全な読み取りアクセスを保証します。 - しかし、実際のイテレーション処理は新しく導入された
v.doLocked(f)
に委譲されるようになりました。これにより、Do
メソッドはロックの取得と解放の責任を持ち、イテレーションのロジックはdoLocked
に集約されます。
- このメソッドは、外部から
-
func (v *Map) doLocked(f func(KeyValue))
の追加:- これは新しく追加されたプライベートなヘルパーメソッドです。
- コメント
// v.mu must be held for reads.
が示すように、このメソッドが呼び出される際には、既にv.mu
の読み取りロックが保持されていることが前提となります。 - このメソッドは、
v.keys
スライスをイテレートし、各キーに対応するKeyValue
ペアを作成して、引数として渡された関数f
に渡します。 - このメソッド自体はロックを取得しないため、既にロックが保持されているコンテキスト(例:
String()
メソッド内)から安全に呼び出すことができます。
この変更により、expvar.Map
の内部的なロック管理がより堅牢になり、再帰的なロック取得によるデッドロックの可能性が排除されました。
関連リンク
- Go issue #7575: https://github.com/golang/go/issues/7575
- Go CL 77540044: https://golang.org/cl/77540044
参考にした情報源リンク
- Go issue #7575 の内容
- Go CL 77540044 の差分
- Go言語の
sync
パッケージとsync.RWMutex
に関する一般的なドキュメント - Go言語の
expvar
パッケージに関する一般的なドキュメント - デッドロックに関する一般的なプログラミングの概念 I have provided the detailed explanation of the commit as requested.