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

[インデックス 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パッケージ内のIntFloatといった型は、それぞれ整数や浮動小数点数を表し、その値をアトミックに(不可分な操作として)更新するためのメソッド(例: Add)を提供しています。これらの更新メソッドは、内部的にsync.Mutexを使用してロックをかけ、複数のゴルーチンからの同時アクセスによる競合状態を防いでいます。

しかし、このコミット以前は、Int型やFloat型の値を文字列として表現するString()メソッドには、このロックが適用されていませんでした。String()メソッドは、expvarが公開するJSON形式の出力や、デバッグ目的で変数の値を表示する際に呼び出されます。もし、String()メソッドが値を読み取っている最中に、別のゴルーチンがAdd()メソッドなどでその値を更新した場合、String()メソッドは不完全または不正な値を読み取ってしまう可能性がありました。これは「競合状態(Race Condition)」として知られる問題であり、プログラムの予測不能な動作や誤ったメトリクス表示につながります。

このコミットは、この潜在的な競合状態を解消し、IntおよびFloatString()メソッドが常に一貫性のある正確な値を返すようにするために行われました。

前提知識の解説

  1. expvarパッケージ: Go言語の標準ライブラリの一つで、プログラムの内部状態をHTTPエンドポイント(通常は/debug/vars)を通じてJSON形式で公開するためのパッケージです。アプリケーションのメトリクス収集やデバッグに利用されます。Int, Float, Map, Stringなどの型を提供し、それぞれが監視対象の変数を表します。

  2. sync.Mutex: Go言語のsyncパッケージに含まれる相互排他ロック(Mutex)です。複数のゴルーチンが共有リソース(この場合はIntFloatの内部値if)に同時にアクセスするのを防ぐために使用されます。

    • Lock(): ロックを取得します。既にロックが取得されている場合、現在のゴルーチンはロックが解放されるまでブロックされます。
    • Unlock(): ロックを解放します。
    • RLock() / RUnlock(): sync.RWMutexで使用される読み取りロック/解放です。複数の読み取りは同時に許可されますが、書き込みは排他的に行われます。Map型のように読み取りが頻繁で書き込みが少ない場合に効率的です。
  3. 競合状態(Race Condition): 複数のゴルーチン(またはスレッド)が共有リソースに同時にアクセスし、そのアクセス順序によってプログラムの実行結果が変わってしまう状態を指します。特に、読み取りと書き込みが同時に行われる場合に問題となりやすく、予期せぬバグやデータ破損を引き起こす可能性があります。

  4. fmt.Stringerインターフェース: Go言語の標準ライブラリfmtパッケージで定義されているインターフェースです。

    type Stringer interface {
        String() string
    }
    

    このインターフェースを実装する型は、String()メソッドを提供し、その型の値を文字列として表現する方法を定義します。fmt.Printfmt.Sprintfなどの関数が、このインターフェースを実装している型に対して自動的にString()メソッドを呼び出し、その戻り値を使用して文字列を生成します。expvarの各型もこのインターフェースを実装しています。

  5. 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)を指定します。
  6. bytes.Buffer: bytesパッケージに含まれる可変長のバイトバッファです。効率的にバイト列を構築するために使用されます。特に、文字列を繰り返し結合する場合に、+演算子による文字列結合よりもパフォーマンスが優れています。fmt.Fprintfio.Writerインターフェースを受け取るため、bytes.Bufferを渡すことができます。

技術的詳細

このコミットの主要な変更点は、expvarパッケージ内のInt型とFloat型のString()メソッドに、それぞれ対応するsync.Mutexのロックとアンロック処理を追加したことです。

変更前は、Int.String()は単にv.i(内部のint64値)をstrconv.FormatIntで文字列に変換し、Float.String()v.f(内部のfloat64値)をstrconv.FormatFloatで文字列に変換していました。これらのメソッドは、v.iv.fといった共有リソースを読み取るだけですが、その読み取り中に別のゴルーチンがAdd()メソッドなどを介してこれらの値を変更する可能性がありました。これにより、String()が読み取る値が、その読み取り操作の途中で変更され、結果として不正な文字列が生成される競合状態が発生し得ました。

修正後のコードでは、String()メソッドの冒頭でv.mu.Lock()を呼び出してロックを取得し、defer v.mu.Unlock()を使ってメソッドの終了時に必ずロックを解放するようにしています。これにより、String()メソッドが実行されている間は、他のゴルーチンがv.iv.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.Fprintfio.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()
 }

コアとなるコードの解説

  1. 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の読み取りがアトミックになり、競合状態が解消されます。

  2. 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の読み取りもスレッドセーフになります。

  3. 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.Fprintfio.Writerインターフェースを引数に取るため、bytes.Bufferのポインタ(&b)を渡す必要があります。この変更は機能的な影響はなく、よりGoらしい慣用的な書き方への修正です。

これらの変更により、expvarパッケージのIntFloatMap型は、その値を文字列として表現する際にも、内部状態へのアクセスが適切に同期され、マルチスレッド環境下での安全性と正確性が向上しました。

関連リンク

参考にした情報源リンク

  • Go言語公式ドキュメント
  • Go言語の並行処理に関する一般的な情報源(例: Go Concurrency Patterns)