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

[インデックス 11612] ファイルの概要

このコミットは、Go言語の標準ライブラリである expvar パッケージのAPIを改訂するものです。具体的には、RemoveAll 関数を公開APIから削除し、Iter 関数および *Map 型の Iter メソッドを Do 関数および (*Map).Do メソッドに置き換える変更が含まれています。これにより、expvar パッケージの利用方法がより効率的かつ安全になります。

コミット

commit 715588f1d3ecc92087018be2aa758c55d1e03d13
Author: David Symonds <dsymonds@golang.org>
Date:   Sat Feb 4 14:32:05 2012 +1100

    expvar: revise API.
    
    Nuke RemoveAll from the public API.
    Replace Iter functions with Do functions.
    
    Fixes #2852.
    
    R=rsc, r
    CC=golang-dev
    https://golang.org/cl/5622055

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/715588f1d3ecc92087018be2aa758c55d1e03d13

元コミット内容

このコミットの元のメッセージは以下の通りです。

expvar: revise API.

Nuke RemoveAll from the public API.
Replace Iter functions with Do functions.

Fixes #2852.

これは、expvar パッケージのAPIを修正し、RemoveAll を公開APIから削除し、Iter 関数を Do 関数に置き換えることを明確に示しています。また、GoのIssue #2852を修正するものであることも言及されています。

変更の背景

この変更の背景には、expvar パッケージの設計改善と、Go 1のリリースに向けたAPIの安定化があります。

  1. RemoveAll の削除: RemoveAll 関数は、公開されているエクスポートされた変数をすべて削除する機能を持っていました。これは主にテスト目的で使用されることが想定されていましたが、本番環境で誤って呼び出されると、アプリケーションの監視データが失われる可能性があり、危険なAPIと見なされました。そのため、公開APIから削除し、テストでのみ利用可能な内部関数として再定義することで、安全性を高めることが目的でした。

  2. Iter から Do への置き換え: 以前の Iter 関数は、エクスポートされた変数をイテレートするためにチャネル (chan KeyValue) を返していました。チャネルを使ったイテレーションはGoのイディオムの一つですが、このケースではいくつかの課題がありました。

    • リソース管理: チャネルは適切にクローズされないとゴルーチンリークを引き起こす可能性があります。
    • 柔軟性の欠如: イテレーション中にカスタムロジックを適用する場合、チャネルから値を受け取ってから処理する必要があり、コードが冗長になることがあります。
    • パフォーマンス: チャネルを介した通信は、直接関数呼び出しを行うよりもオーバーヘッドが大きい場合があります。

    Do 関数は、イテレーションロジックを抽象化し、ユーザーが提供するクロージャ(コールバック関数)を各要素に適用するパターンを採用しています。このパターンは、よりシンプルで、リソース管理が容易であり、パフォーマンスも向上する可能性があります。また、イテレーション中にマップがロックされることが明示され、既存のエントリは並行して更新される可能性があるという保証が提供されます。

  3. 並行性制御の改善: expvar パッケージは、アプリケーションの実行中に動的に変化する統計情報を公開するため、並行アクセスに対する安全性が非常に重要です。このコミットでは、sync.Mutexsync.RWMutex に変更することで、読み取り操作の並行性を高め、パフォーマンスを向上させています。

これらの変更は、expvar パッケージをより堅牢で、安全で、効率的なものにすることを目的としていました。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念と標準ライブラリの知識が必要です。

  1. expvar パッケージ:

    • Go言語の標準ライブラリの一つで、実行中のGoプログラムの内部状態(変数)をHTTP経由で公開するためのパッケージです。
    • 主に、アプリケーションのメトリクス(カウンター、ゲージなど)やデバッグ情報を /debug/vars エンドポイントでJSON形式で提供するために使用されます。
    • Var インターフェースを実装する型(Int, Float, String, Func, Map など)をエクスポートできます。
  2. sync パッケージ:

    • Go言語で並行処理を行う際に、共有リソースへのアクセスを同期するためのプリミティブを提供します。
    • sync.Mutex: 排他ロック(相互排他ロック)を提供します。一度に一つのゴルーチンだけがロックを取得でき、共有リソースへのアクセスを保護します。読み取りと書き込みの両方でロックが必要です。
    • sync.RWMutex (Reader-Writer Mutex): 読み取り/書き込みロックを提供します。
      • 複数のゴルーチンが同時に読み取りロックを取得できます(共有ロック)。
      • 書き込みロックは排他的であり、書き込みロックが取得されている間は、他の読み取りロックも書き込みロックも取得できません。
      • 読み取り操作が書き込み操作よりもはるかに多い場合に、sync.Mutex よりも高い並行性を実現できます。
    • Lock() / Unlock(): sync.Mutex および sync.RWMutex の書き込みロックを取得/解放します。
    • RLock() / RUnlock(): sync.RWMutex の読み取りロックを取得/解放します。
    • defer ステートメント: 関数の終了時に実行されるように、関数の呼び出しをスケジュールします。ロックの解放によく使用され、ロック忘れを防ぎます。
  3. チャネル (Channels):

    • ゴルーチン間で値を送受信するための通信メカニズムです。
    • make(chan Type) で作成し、<- 演算子で送受信します。
    • close(chan) でチャネルを閉じることができます。
    • このコミットでは、Iter 関数がチャネルを介して値をストリームしていましたが、Do 関数ではクロージャ(コールバック)に置き換えられています。
  4. クロージャ (Closures) / コールバック関数:

    • Go言語では、関数を第一級オブジェクトとして扱うことができます。つまり、関数を変数に代入したり、引数として渡したり、戻り値として返したりできます。
    • クロージャは、それが定義された環境の変数を「キャプチャ」する関数です。
    • コールバック関数は、他の関数に引数として渡され、特定のイベントが発生したときや処理が完了したときに呼び出される関数です。Do 関数はこのパターンを使用しています。
  5. Go 1の互換性保証:

    • Go 1は、Go言語の最初の安定版リリースであり、将来のバージョンとの互換性が厳密に保証されました。このコミットは、Go 1リリース前のAPIレビューと安定化の一環として行われました。

技術的詳細

このコミットにおける技術的な変更点は多岐にわたりますが、主に以下の3つの柱に集約されます。

  1. sync.Mutex から sync.RWMutex への移行:

    • expvar パッケージの Map 型(エクスポートされた変数のマップ)と、グローバルな vars マップ(すべてのエクスポートされた変数を保持)は、共有リソースであり、複数のゴルーチンから同時にアクセスされる可能性があります。
    • 以前は sync.Mutex を使用していましたが、これは読み取り操作であっても排他ロックが必要でした。
    • このコミットでは、Mapmu フィールドとグローバルな mutex 変数が sync.Mutex から sync.RWMutex に変更されました。
    • これにより、String()Get() のような読み取り専用の操作では RLock()RUnlock() を使用できるようになり、複数の読み取りゴルーチンが同時に実行できるようになりました。
    • Add()AddFloat() のような書き込み操作では、依然として Lock()Unlock() を使用しますが、これらの関数内でのマップの存在チェック(読み取り操作)には RLock() を使用し、実際にマップを更新する部分(書き込み操作)でのみ Lock() を取得するという最適化が行われています。これは、マップにキーが存在しない場合にのみ書き込みロックが必要となるため、ロックの粒度を細かくすることで並行性を向上させるためのパターンです。
  2. Iter 関数/メソッドから Do 関数/メソッドへの置き換え:

    • 旧API (Iter):
      func (v *Map) Iter() <-chan KeyValue {
          c := make(chan KeyValue)
          go v.iterate(c)
          return c
      }
      func Iter() <-chan KeyValue {
          c := make(chan KeyValue)
          go iterate(c)
          return c
      }
      
      これらの関数は、KeyValue 型のチャネルを返し、別のゴルーチンでマップをイテレートしてチャネルに値を送信していました。ユーザーはチャネルから値を受け取ることでイテレーションを行いました。
    • 新API (Do):
      func (v *Map) Do(f func(KeyValue)) {
          v.mu.RLock()
          defer v.mu.RUnlock()
          for k, v := range v.m {
              f(KeyValue{k, v})
          }
      }
      func Do(f func(KeyValue)) {
          mutex.RLock()
          defer mutex.RUnlock()
          for k, v := range vars {
              f(KeyValue{k, v})
          }
      }
      
      新しい Do 関数は、func(KeyValue) 型のクロージャを引数として受け取ります。イテレーション中に各 KeyValue ペアに対してこのクロージャが呼び出されます。これにより、チャネルを介した通信のオーバーヘッドがなくなり、ユーザーはイテレーションロジックを直接 Do 関数に渡せるため、コードがより簡潔になります。また、Do 関数内で読み取りロックが取得され、イテレーション中はマップがロックされることが保証されますが、既存のエントリは並行して更新される可能性があるという注意書きが追加されています。
  3. RemoveAll の公開APIからの削除とテスト専用化:

    • expvar.go から RemoveAll() 関数が完全に削除されました。
    • しかし、expvar_test.go には、テスト目的でのみ使用される新しい RemoveAll() 関数が追加されました。これは、テスト環境でエクスポートされた変数をリセットする必要があるためです。このテスト専用の RemoveAll は、グローバルな vars マップを新しい空のマップに置き換えることで機能します。
  4. http.Handle から http.HandleFunc への変更:

    • init() 関数内で、http.Handle("/debug/vars", http.HandlerFunc(expvarHandler))http.HandleFunc("/debug/vars", expvarHandler) に変更されました。
    • http.Handlehttp.Handler インターフェースを実装するオブジェクトを受け取りますが、http.HandleFuncfunc(ResponseWriter, *Request) 型の関数を直接受け取ります。これは単なるAPIの簡略化であり、機能的な変更はありません。

これらの変更は、Go 1のリリースに向けて、expvar パッケージのAPIをより現代的で、安全で、効率的なものにするための重要なステップでした。

コアとなるコードの変更箇所

主要な変更は src/pkg/expvar/expvar.go に集中しています。

  1. Map struct の mu フィールドの変更:

    --- a/src/pkg/expvar/expvar.go
    +++ b/src/pkg/expvar/expvar.go
    @@ -83,7 +83,7 @@ func (v *Float) Set(value float64) {
     // Map is a string-to-Var map variable that satisfies the Var interface.
     type Map struct {
     	m  map[string]Var
    -	mu sync.Mutex
    +	mu sync.RWMutex
     }
    
  2. Map メソッドでの sync.RWMutex の使用:

    • String()Get()RLock/RUnlock を使用。
    • Add()AddFloat() で、存在チェックに RLock/RUnlock を使用し、必要に応じて書き込みロック (Lock/Unlock) を取得するパターンに変更。
  3. Map.Iter() の削除と Map.Do() の追加:

    --- a/src/pkg/expvar/expvar.go
    +++ b/src/pkg/expvar/expvar.go
    @@ -157,18 +167,15 @@ func (v *Map) AddFloat(key string, delta float64) {
     	}
     }
     
    -// TODO(rsc): Make sure map access in separate thread is safe.\n-func (v *Map) iterate(c chan<- KeyValue) {\n+// Do calls f for each entry in the map.
    +// The map is locked during the iteration,
    +// but existing entries may be concurrently updated.
    +func (v *Map) Do(f func(KeyValue)) {
    +	v.mu.RLock()
    +	defer v.mu.RUnlock()
     	for k, v := range v.m {
    -\t\tc <- KeyValue{k, v}\n+\t\tf(KeyValue{k, v})\n     	}
    -\tclose(c)\n-}
    -\n-func (v *Map) Iter() <-chan KeyValue {\n-\tc := make(chan KeyValue)\n-\tgo v.iterate(c)\n-\treturn c
     }
    
  4. グローバルな varsmutex の変更:

    --- a/src/pkg/expvar/expvar.go
    +++ b/src/pkg/expvar/expvar.go
    @@ -190,8 +197,10 @@ func (f Func) String() string {
     }
     
     // All published variables.
    -var vars map[string]Var = make(map[string]Var)
    -var mutex sync.Mutex
    +var (
    +	mutex sync.RWMutex
    +	vars  map[string]Var = make(map[string]Var)
    +)
    
  5. Get() 関数での RLock/RUnlock の使用:

    --- a/src/pkg/expvar/expvar.go
    +++ b/src/pkg/expvar/expvar.go
    @@ -207,17 +216,11 @@ func Publish(name string, v Var) {
     
     // Get retrieves a named exported variable.
     func Get(name string) Var {
    +\tmutex.RLock()
    +\tdefer mutex.RUnlock()
     	return vars[name]
     }
    
  6. RemoveAll() の削除:

    --- a/src/pkg/expvar/expvar.go
    +++ b/src/pkg/expvar/expvar.go
    @@ -216,12 +219,6 @@ func Get(name string) Var {
     	return vars[name]
     }
     
    -// RemoveAll removes all exported variables.
    -// This is for tests; don't call this on a real server.
    -func RemoveAll() {
    -	mutex.Lock()
    -	defer mutex.Unlock()
    -	vars = make(map[string]Var)
    -}
    -
     // Convenience functions for creating new exported variables.
    
  7. グローバルな Iter() の削除と Do() の追加:

    --- a/src/pkg/expvar/expvar.go
    +++ b/src/pkg/expvar/expvar.go
    @@ -244,31 +247,28 @@ func NewString(name string) *String {
     	return v
     }
     
    -// TODO(rsc): Make sure map access in separate thread is safe.\n-func iterate(c chan<- KeyValue) {\n+// Do calls f for each exported variable.
    +// The global variable map is locked during the iteration,
    +// but existing entries may be concurrently updated.
    +func Do(f func(KeyValue)) {
    +	mutex.RLock()
    +	defer mutex.RUnlock()
     	for k, v := range vars {
    -\t\tc <- KeyValue{k, v}\n+\t\tf(KeyValue{k, v})\n     	}
    -\tclose(c)\n-}
    -\n-func Iter() <-chan KeyValue {\n-\tc := make(chan KeyValue)\n-\tgo iterate(c)\n-\treturn c
     }
    
  8. expvarHandler での Do 関数の使用:

    --- a/src/pkg/expvar/expvar.go
    +++ b/src/pkg/expvar/expvar.go
    @@ -275,11 +275,11 @@ func expvarHandler(w http.ResponseWriter, r *http.Request) {
      \tw.Header().Set("Content-Type", "application/json; charset=utf-8")
      \tfmt.Fprintf(w, "{\\n")
      \tfirst := true
    -\tfor name, value := range vars {\n+\tDo(func(kv KeyValue) {
      \t\tif !first {
      \t\t\tfmt.Fprintf(w, ",\\n")
      \t\t}
      \t\tfirst = false
    -\t\tfmt.Fprintf(w, "%q: %s", name, value)\n-\t}\n+\t\tfmt.Fprintf(w, "%q: %s", kv.Key, kv.Value)
    +\t})
      \tfmt.Fprintf(w, "\\n}\\n")
     }
    
  9. init() 関数での http.HandleFunc の使用:

    --- a/src/pkg/expvar/expvar.go
    +++ b/src/pkg/expvar/expvar.go
    @@ -281,7 +281,7 @@ func memstats() interface{} {
     }
     
     func init() {
    -\thttp.Handle("/debug/vars", http.HandlerFunc(expvarHandler))\n+\thttp.HandleFunc("/debug/vars", expvarHandler)
     	Publish("cmdline", Func(cmdline))
     	Publish("memstats", Func(memstats))
     }
    
  10. src/pkg/expvar/expvar_test.go での RemoveAll() の再追加(テスト専用):

    --- a/src/pkg/expvar/expvar_test.go
    +++ b/src/pkg/expvar/expvar_test.go
    @@ -9,6 +9,14 @@ import (
      	"testing"
      )
      
    +// RemoveAll removes all exported variables.
    +// This is for tests only.
    +func RemoveAll() {
    +	mutex.Lock()
    +	defer mutex.Unlock()
    +	vars = make(map[string]Var)
    +}
    +
     func TestInt(t *testing.T) {
      	reqs := NewInt("requests")
      	if reqs.i != 0 {
    

コアとなるコードの解説

このコミットのコアとなる変更は、expvar パッケージの並行性制御とイテレーションメカニズムの改善です。

  1. sync.RWMutex の導入と利用パターン:

    • Map 型とグローバルな vars マップは、アプリケーションの実行中に頻繁に読み取られる一方で、書き込み(変数の追加や更新)は比較的少ないという特性があります。
    • sync.Mutex は読み取りと書き込みの両方で排他ロックを必要とするため、読み取り操作が多い場合にボトルネックになる可能性があります。
    • sync.RWMutex は、複数の読み取りゴルーチンが同時にアクセスできる「読み取りロック」と、排他的な「書き込みロック」を提供します。
    • Map.String()Map.Get()expvar.Get()expvar.Do()Map.Do() のような読み取り専用の操作では RLock()RUnlock() を使用することで、これらの操作の並行性を大幅に向上させています。
    • Map.Add()Map.AddFloat() のような書き込み操作では、まず RLock() を取得してマップにキーが存在するかどうかを確認します。もしキーが存在しない場合(つまり、新しい変数を追加する必要がある場合)にのみ、RUnlock() を解放し、Lock() を取得して排他的にマップを更新します。この「二段階ロック」または「最適化された書き込み」パターンにより、不要な書き込みロックの取得を避け、並行性をさらに高めています。
  2. Iter から Do へのパラダイムシフト:

    • 以前の Iter 関数は、チャネルを介して値をストリームするGoのイディオムを使用していました。これは柔軟性がある一方で、チャネルの作成、ゴルーチンの起動、チャネルへの送信、チャネルからの受信といったオーバーヘッドがありました。また、チャネルのクローズを適切に行わないとリソースリークのリスクもありました。
    • 新しい Do 関数は、コールバック関数(クロージャ)を受け取るパターンを採用しています。このパターンは、イテレーションロジックを直接 Do 関数に渡すため、コードがより簡潔で読みやすくなります。
    • Do 関数内で RLock() を取得し、イテレーションが完了するまでロックを保持することで、イテレーション中のマップの一貫性を保証しています。ただし、Do 関数のコメントにあるように、「既存のエントリは並行して更新される可能性がある」という点に注意が必要です。これは、Do が読み取りロックを使用しているため、他のゴルーチンが書き込みロックを取得して既存の値を変更する可能性があることを意味します。新しいエントリの追加は、イテレーション中に発生しないようにロックによって保護されています。
    • この変更は、expvarHandler のような内部的なイテレーション処理にも適用され、コードの統一性と効率性が向上しています。
  3. RemoveAll の公開APIからの削除:

    • RemoveAll は、本番環境での誤用を防ぐために公開APIから削除されました。これは、Go 1のAPI安定化と安全性向上の哲学に沿ったものです。
    • テストコードでのみ必要とされるため、expvar_test.go にテスト専用の RemoveAll 関数が追加されました。これにより、テストの分離性と再現性が保たれます。

これらの変更は、expvar パッケージが提供する監視機能の堅牢性、パフォーマンス、および安全性を向上させるための重要なステップでした。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード(特に src/pkg/expvar/ ディレクトリ)
  • Go言語のIssueトラッカー (GitHub)
  • sync.Mutexsync.RWMutex に関する一般的なGo言語の並行性に関する記事やチュートリアル。