[インデックス 12381] ファイルの概要
このコミットは、Go言語のexpvar
パッケージにおいて、Int
型およびFloat
型のString()
メソッドに不足していたミューテックスロックを追加し、スレッドセーフティを確保することを目的としています。これにより、これらの変数が同時に読み取られたり更新されたりする際に発生しうる競合状態(レースコンディション)を防ぎ、正確な値の文字列表現を保証します。また、Map
型のString()
メソッドにおけるbytes.Buffer
の初期化方法を、より慣用的な形式に修正しています。
コミット
commit 1042d7d5efe7ce90f3c3bba38e8c78e2b9c63172
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Mar 5 11:09:50 2012 -0800
expvar: add missing locking in String methods
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5726062
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1042d7d5efe7ce90f3c3bba38e8c78e2b9c63172
元コミット内容
expvar: add missing locking in String methods
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5726062
変更の背景
Go言語のexpvar
パッケージは、実行中のプログラムの内部状態(変数)をHTTP経由で公開するための標準パッケージです。これにより、アプリケーションのメトリクスやデバッグ情報を簡単に監視できます。expvar
パッケージ内のInt
やFloat
といった型は、それぞれ整数や浮動小数点数を表し、その値をアトミックに(不可分な操作として)更新するためのメソッド(例: Add
)を提供しています。これらの更新メソッドは、内部的にsync.Mutex
を使用してロックをかけ、複数のゴルーチンからの同時アクセスによる競合状態を防いでいます。
しかし、このコミット以前は、Int
型やFloat
型の値を文字列として表現するString()
メソッドには、このロックが適用されていませんでした。String()
メソッドは、expvar
が公開するJSON形式の出力や、デバッグ目的で変数の値を表示する際に呼び出されます。もし、String()
メソッドが値を読み取っている最中に、別のゴルーチンがAdd()
メソッドなどでその値を更新した場合、String()
メソッドは不完全または不正な値を読み取ってしまう可能性がありました。これは「競合状態(Race Condition)」として知られる問題であり、プログラムの予測不能な動作や誤ったメトリクス表示につながります。
このコミットは、この潜在的な競合状態を解消し、Int
およびFloat
のString()
メソッドが常に一貫性のある正確な値を返すようにするために行われました。
前提知識の解説
-
expvar
パッケージ: Go言語の標準ライブラリの一つで、プログラムの内部状態をHTTPエンドポイント(通常は/debug/vars
)を通じてJSON形式で公開するためのパッケージです。アプリケーションのメトリクス収集やデバッグに利用されます。Int
,Float
,Map
,String
などの型を提供し、それぞれが監視対象の変数を表します。 -
sync.Mutex
: Go言語のsync
パッケージに含まれる相互排他ロック(Mutex)です。複数のゴルーチンが共有リソース(この場合はInt
やFloat
の内部値i
やf
)に同時にアクセスするのを防ぐために使用されます。Lock()
: ロックを取得します。既にロックが取得されている場合、現在のゴルーチンはロックが解放されるまでブロックされます。Unlock()
: ロックを解放します。RLock()
/RUnlock()
:sync.RWMutex
で使用される読み取りロック/解放です。複数の読み取りは同時に許可されますが、書き込みは排他的に行われます。Map
型のように読み取りが頻繁で書き込みが少ない場合に効率的です。
-
競合状態(Race Condition): 複数のゴルーチン(またはスレッド)が共有リソースに同時にアクセスし、そのアクセス順序によってプログラムの実行結果が変わってしまう状態を指します。特に、読み取りと書き込みが同時に行われる場合に問題となりやすく、予期せぬバグやデータ破損を引き起こす可能性があります。
-
fmt.Stringer
インターフェース: Go言語の標準ライブラリfmt
パッケージで定義されているインターフェースです。type Stringer interface { String() string }
このインターフェースを実装する型は、
String()
メソッドを提供し、その型の値を文字列として表現する方法を定義します。fmt.Print
やfmt.Sprintf
などの関数が、このインターフェースを実装している型に対して自動的にString()
メソッドを呼び出し、その戻り値を使用して文字列を生成します。expvar
の各型もこのインターフェースを実装しています。 -
strconv
パッケージ: Go言語の標準ライブラリで、文字列と基本的なデータ型(整数、浮動小数点数、真偽値など)との間の変換を提供します。strconv.FormatInt(i int64, base int)
:int64
型の整数を、指定された基数(例: 10進数なら10)の文字列に変換します。strconv.FormatFloat(f float64, fmt byte, prec int, bitSize int)
:float64
型の浮動小数点数を文字列に変換します。fmt
はフォーマット文字(例: 'g')、prec
は精度、bitSize
は浮動小数点数のビットサイズ(32または64)を指定します。
-
bytes.Buffer
:bytes
パッケージに含まれる可変長のバイトバッファです。効率的にバイト列を構築するために使用されます。特に、文字列を繰り返し結合する場合に、+
演算子による文字列結合よりもパフォーマンスが優れています。fmt.Fprintf
はio.Writer
インターフェースを受け取るため、bytes.Buffer
を渡すことができます。
技術的詳細
このコミットの主要な変更点は、expvar
パッケージ内のInt
型とFloat
型のString()
メソッドに、それぞれ対応するsync.Mutex
のロックとアンロック処理を追加したことです。
変更前は、Int.String()
は単にv.i
(内部のint64
値)をstrconv.FormatInt
で文字列に変換し、Float.String()
はv.f
(内部のfloat64
値)をstrconv.FormatFloat
で文字列に変換していました。これらのメソッドは、v.i
やv.f
といった共有リソースを読み取るだけですが、その読み取り中に別のゴルーチンがAdd()
メソッドなどを介してこれらの値を変更する可能性がありました。これにより、String()
が読み取る値が、その読み取り操作の途中で変更され、結果として不正な文字列が生成される競合状態が発生し得ました。
修正後のコードでは、String()
メソッドの冒頭でv.mu.Lock()
を呼び出してロックを取得し、defer v.mu.Unlock()
を使ってメソッドの終了時に必ずロックを解放するようにしています。これにより、String()
メソッドが実行されている間は、他のゴルーチンがv.i
やv.f
を更新するAdd()
などのメソッドを呼び出すことができなくなり、値の読み取りがアトミックに行われることが保証されます。
Map
型のString()
メソッドについては、既にv.mu.RLock()
とdefer v.mu.RUnlock()
が適用されており、読み取り操作の競合状態は防止されていました。このコミットでは、bytes.Buffer
の初期化方法がb := new(bytes.Buffer)
からvar b bytes.Buffer
に変更されています。これは機能的な変更ではなく、Go言語におけるbytes.Buffer
のより慣用的な初期化方法への修正です。new(bytes.Buffer)
はポインタを返しますが、var b bytes.Buffer
は値型を宣言し、bytes.Buffer
はゼロ値が有効な状態(内部的にnilスライスを持つ)であるため、直接使用できます。fmt.Fprintf
はio.Writer
インターフェースを受け取るため、&b
としてアドレスを渡す必要があります。
コアとなるコードの変更箇所
src/pkg/expvar/expvar.go
--- a/src/pkg/expvar/expvar.go
+++ b/src/pkg/expvar/expvar.go
@@ -44,7 +44,11 @@ type Int struct {
mu sync.Mutex
}
-func (v *Int) String() string { return strconv.FormatInt(v.i, 10) }
+func (v *Int) String() string {
+ v.mu.Lock()
+ defer v.mu.Unlock()
+ return strconv.FormatInt(v.i, 10)
+}
func (v *Int) Add(delta int64) {
v.mu.Lock()
@@ -64,7 +68,11 @@ type Float struct {
mu sync.Mutex
}
-func (v *Float) String() string { return strconv.FormatFloat(v.f, 'g', -1, 64) }
+func (v *Float) String() string {
+ v.Lock()
+ defer v.Unlock()
+ return strconv.FormatFloat(v.f, 'g', -1, 64)
+}
// Add adds delta to v.
func (v *Float) Add(delta float64) {
@@ -95,17 +103,17 @@ type KeyValue struct {
func (v *Map) String() string {
v.mu.RLock()
defer v.mu.RUnlock()
- b := new(bytes.Buffer)
- fmt.Fprintf(b, "{")
+ var b bytes.Buffer
+ fmt.Fprintf(&b, "{")
first := true
for key, val := range v.m {
if !first {
- fmt.Fprintf(b, ", ")
+ fmt.Fprintf(&b, ", ")
}
- fmt.Fprintf(b, "\"%s\": %v\", key, val)
+ fmt.Fprintf(&b, "\"%s\": %v\", key, val)
first = false
}
- fmt.Fprintf(b, "}")
+ fmt.Fprintf(&b, "}")
return b.String()
}
コアとなるコードの解説
-
Int.String()
メソッドの変更:func (v *Int) String() string { v.mu.Lock() // ロックを取得 defer v.mu.Unlock() // メソッド終了時にロックを解放 return strconv.FormatInt(v.i, 10) }
Int
型の内部値v.i
を文字列に変換する前に、v.mu.Lock()
を呼び出してミューテックスロックを取得します。これにより、このString()
メソッドが実行されている間は、他のゴルーチンがv.i
を書き換えることができなくなります。defer v.mu.Unlock()
は、String()
メソッドが正常に終了するか、パニックが発生するかにかかわらず、必ずロックが解放されることを保証します。これにより、v.i
の読み取りがアトミックになり、競合状態が解消されます。 -
Float.String()
メソッドの変更:func (v *Float) String() string { v.mu.Lock() // ロックを取得 defer v.mu.Unlock() // メソッド終了時にロックを解放 return strconv.FormatFloat(v.f, 'g', -1, 64) }
Int.String()
と同様に、Float
型の内部値v.f
を文字列に変換する前にロックを取得し、メソッド終了時に解放するように変更されました。これにより、v.f
の読み取りもスレッドセーフになります。 -
Map.String()
メソッドの変更:func (v *Map) String() string { v.mu.RLock() defer v.mu.RUnlock() var b bytes.Buffer // new(bytes.Buffer) から var b bytes.Buffer に変更 fmt.Fprintf(&b, "{") // b ではなく &b を渡す first := true for key, val := range v.m { if !first { fmt.Fprintf(&b, ", ") // b ではなく &b を渡す } fmt.Fprintf(&b, "\"%s\": %v\", key, val) // b ではなく &b を渡す first = false } fmt.Fprintf(&b, "}") // b ではなく &b を渡す return b.String() }
bytes.Buffer
の初期化がb := new(bytes.Buffer)
からvar b bytes.Buffer
に変更されました。bytes.Buffer
は値型であり、ゼロ値が有効な状態であるため、new
を使ってポインタを生成する必要はありません。fmt.Fprintf
はio.Writer
インターフェースを引数に取るため、bytes.Buffer
のポインタ(&b
)を渡す必要があります。この変更は機能的な影響はなく、よりGoらしい慣用的な書き方への修正です。
これらの変更により、expvar
パッケージのInt
、Float
、Map
型は、その値を文字列として表現する際にも、内部状態へのアクセスが適切に同期され、マルチスレッド環境下での安全性と正確性が向上しました。
関連リンク
- Go言語
expvar
パッケージドキュメント: https://pkg.go.dev/expvar - Go言語
sync
パッケージドキュメント: https://pkg.go.dev/sync - Go言語
strconv
パッケージドキュメント: https://pkg.go.dev/strconv - Go言語
bytes
パッケージドキュメント: https://pkg.go.dev/bytes - Go言語
fmt
パッケージドキュメント: https://pkg.go.dev/fmt
参考にした情報源リンク
- Go言語公式ドキュメント
- Go言語の並行処理に関する一般的な情報源(例: Go Concurrency Patterns)