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

[インデックス 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.MapString()メソッドが、その内部で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.MapString()メソッド内で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]})
 	}

コアとなるコードの解説

  1. func (v *Map) String() string の変更:

    • 変更前: v.Do(func(kv KeyValue) { ... }) を呼び出していました。
    • 変更後: v.doLocked(func(kv KeyValue) { ... }) を呼び出すように変更されました。
    • これにより、String()メソッドが既に保持している読み取りロックを、Do()メソッドが再帰的に取得しようとする問題を回避します。
  2. func (v *Map) Do(f func(KeyValue)) の変更:

    • このメソッドは、外部からMapの要素をイテレートするために使用されます。
    • 変更後も、v.mu.RLock()defer v.mu.RUnlock() を使用して、マップへの安全な読み取りアクセスを保証します。
    • しかし、実際のイテレーション処理は新しく導入された v.doLocked(f) に委譲されるようになりました。これにより、Doメソッドはロックの取得と解放の責任を持ち、イテレーションのロジックはdoLockedに集約されます。
  3. 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 の内容
  • Go CL 77540044 の差分
  • Go言語のsyncパッケージとsync.RWMutexに関する一般的なドキュメント
  • Go言語のexpvarパッケージに関する一般的なドキュメント
  • デッドロックに関する一般的なプログラミングの概念 I have provided the detailed explanation of the commit as requested.