[インデックス 14092] ファイルの概要
このコミットは、Go言語の標準ライブラリである encoding/gob
パッケージにおけるデータ競合(data race)の修正を目的としています。具体的には、Register
関数および関連する型登録メカニズムにおいて、並行アクセス時に発生しうる競合状態を解消するために、読み書きロック(sync.RWMutex
)が導入されました。
コミット
commit e855fcc3073ccb4dc3fa7a3b7dc2b076e5bb54cf
Author: Rob Pike <r@golang.org>
Date: Tue Oct 9 11:56:38 2012 +1100
encoding/gob: fix data race in Register
Fixes #4214.
R=golang-dev, dsymonds, bradfitz
CC=golang-dev
https://golang.org/cl/6637047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e855fcc3073ccb4dc3fa7a3b7dc2b076e5bb54cf
元コミット内容
encoding/gob: fix data race in Register
Fixes #4214.
R=golang-dev, dsymonds, bradfitz
CC=golang-dev
https://golang.org/cl/6637047
変更の背景
encoding/gob
パッケージは、Goのデータ構造をシリアライズおよびデシリアライズするためのメカニズムを提供します。このパッケージでは、インターフェース型や特定の具象型をエンコード/デコードするために、内部的に型情報を登録・管理する仕組みがあります。この型情報の登録や参照は、Register
関数や関連する内部マップ(nameToConcreteType
、concreteTypeToName
)を通じて行われます。
元の実装では、これらの型情報を管理するマップへのアクセスが同期されていませんでした。そのため、複数のゴルーチンが同時に型を登録しようとしたり、登録された型情報を参照しようとしたりすると、データ競合が発生する可能性がありました。データ競合は、プログラムの予測不能な動作、クラッシュ、または誤ったデータの読み取りにつながる深刻なバグです。
この問題は、GoのIssueトラッカーで #4214 として報告されていました。このコミットは、その報告されたデータ競合を修正することを目的としています。
前提知識の解説
Go言語におけるデータ競合 (Data Race)
データ競合は、複数のゴルーチンが同時に同じメモリ位置にアクセスし、そのうち少なくとも1つのアクセスが書き込みである場合に発生します。さらに、これらのアクセスが同期メカニズムによって順序付けされていない場合にデータ競合とみなされます。データ競合はGoのメモリモデルにおいて未定義の動作を引き起こし、プログラムの信頼性を著しく損ないます。
sync
パッケージと同期プリミティブ
Go言語は、並行処理におけるデータ競合を防ぐために、標準ライブラリの sync
パッケージに様々な同期プリミティブを提供しています。
sync.Mutex
: 排他ロック(Mutual Exclusion Lock)を提供します。Lock()
とUnlock()
メソッドを持ち、一度に1つのゴルーチンのみが保護されたセクションにアクセスできるようにします。読み書き両方の操作を排他的に保護する場合に適しています。sync.RWMutex
: 読み書きロック(Reader-Writer Mutex)を提供します。これは、読み取り操作が頻繁に行われ、書き込み操作が比較的少ない場合に効率的です。RLock()
/RUnlock()
: 読み取りロックを取得/解放します。複数のゴルーチンが同時に読み取りロックを取得できます。Lock()
/Unlock()
: 書き込みロックを取得/解放します。書き込みロックが取得されている間は、他のすべての読み取りロックおよび書き込みロックはブロックされます。
encoding/gob
パッケージ
encoding/gob
パッケージは、Goのプログラム間でGoの値をエンコード(シリアライズ)およびデコード(デシリアライズ)するためのバイナリ形式を提供します。これは、RPC(Remote Procedure Call)や永続化などの用途でよく使用されます。gob
は、エンコード/デコードするデータの型情報を自動的に交換し、必要に応じて型を登録する機能を持っています。
技術的詳細
このコミットの主要な変更点は、encoding/gob
パッケージ内で型情報を管理するグローバルマップ nameToConcreteType
と concreteTypeToName
へのアクセスを sync.RWMutex
を使用して同期することです。
nameToConcreteType
: 型の名前(文字列)からreflect.Type
へのマッピングを保持します。concreteTypeToName
:reflect.Type
から型名(文字列)へのマッピングを保持します。
これらのマップは、gob
がインターフェース型をエンコード/デコードする際に、具象型とそれに対応する名前を解決するために使用されます。
sync.RWMutex
の選択理由
sync.RWMutex
が選択されたのは、これらのマップへのアクセスパターンが「読み取りが頻繁で、書き込み(登録)が比較的少ない」という特性を持つためです。
- 読み取り操作:
decode.go
のdecodeInterface
関数やencode.go
のencodeInterface
関数、type_test.go
のテストコードなど、既存の型情報を参照する際にはRLock()
とRUnlock()
を使用します。これにより、複数のゴルーチンが同時に型情報を安全に読み取ることができます。 - 書き込み操作:
type.go
のRegisterName
関数(およびRegister
関数から呼ばれる)のように、新しい型を登録する際にはLock()
とUnlock()
を使用します。これにより、型情報の変更中に他の読み取りや書き込みがブロックされ、マップの一貫性が保たれます。
sync.Mutex
を使用することも可能ですが、その場合、読み取り操作であっても排他ロックが必要となり、並行性が低下します。sync.RWMutex
を使用することで、読み取り操作の並行性を維持しつつ、書き込み操作の安全性を確保できるため、より効率的な解決策となります。
コアとなるコードの変更箇所
以下のファイルが変更されています。
src/pkg/encoding/gob/decode.go
:decodeInterface
関数内でnameToConcreteType
マップを読み取る際に、registerLock.RLock()
とregisterLock.RUnlock()
が追加されました。
src/pkg/encoding/gob/encode.go
:encodeInterface
関数内でconcreteTypeToName
マップを読み取る際に、registerLock.RLock()
とregisterLock.RUnlock()
が追加されました。
src/pkg/encoding/gob/type.go
:registerLock sync.RWMutex
がグローバル変数として追加されました。RegisterName
関数内で型を登録する際に、registerLock.Lock()
とdefer registerLock.Unlock()
が追加されました。
src/pkg/encoding/gob/type_test.go
:TestRegistrationNaming
テスト関数内でnameToConcreteType
マップを読み取る際に、registerLock.RLock()
とregisterLock.RUnlock()
が追加されました。これは、テスト自体がデータ競合を引き起こさないようにするため、またはテストが実際の使用状況をより正確に反映するようにするための変更です。
コアとなるコードの解説
src/pkg/encoding/gob/type.go
の変更
var (
registerLock sync.RWMutex // 追加
nameToConcreteType = make(map[string]reflect.Type)
concreteTypeToName = make(map[reflect.Type]string)
)
func RegisterName(name string, value interface{}) {
if name == "" {
// reserved for nil
panic("attempt to register empty name")
}
registerLock.Lock() // 追加
defer registerLock.Unlock() // 追加
ut := userType(reflect.TypeOf(value))
// ... (既存のロジック)
}
registerLock sync.RWMutex
:nameToConcreteType
とconcreteTypeToName
マップへのアクセスを保護するための読み書きロックが宣言されました。registerLock.Lock()
とdefer registerLock.Unlock()
:RegisterName
関数は、新しい型を登録する際にこれらのマップを書き換えます。そのため、書き込みロックを取得し、関数が終了する際に確実に解放するようにdefer
を使用しています。これにより、型登録中のマップへの排他的アクセスが保証され、他のゴルーチンからの同時書き込みや読み取りがブロックされます。
src/pkg/encoding/gob/decode.go
の変更
func (dec *Decoder) decodeInterface(ityp reflect.Type, state *decoderState, p uint64) (reflect.Value, error) {
// ... (既存のロジック)
// The concrete type must be registered.
registerLock.RLock() // 追加
typ, ok := nameToConcreteType[name]
registerLock.RUnlock() // 追加
if !ok {
errorf("name not registered for interface: %q", name)
}
// ... (既存のロジック)
}
registerLock.RLock()
とregisterLock.RUnlock()
:decodeInterface
関数は、受信したデータから型名を取得し、それに対応する具象型をnameToConcreteType
マップから検索します。これは読み取り操作であるため、読み取りロックを取得し、検索後に解放します。これにより、複数のデコーダが同時に型情報を読み取ることが可能になります。
src/pkg/encoding/gob/encode.go
の変更
func (enc *Encoder) encodeInterface(b *bytes.Buffer, iv reflect.Value) {
// ... (既存のロジック)
ut := userType(iv.Elem().Type())
registerLock.RLock() // 追加
name, ok := concreteTypeToName[ut.base]
registerLock.RUnlock() // 追加
if !ok {
errorf("type not registered for interface: %s", ut.base)
}
// ... (既存のロジック)
}
registerLock.RLock()
とregisterLock.RUnlock()
:encodeInterface
関数は、エンコードするインターフェースの具象型に対応する名前をconcreteTypeToName
マップから検索します。これも読み取り操作であるため、読み取りロックを使用します。
これらの変更により、encoding/gob
パッケージの型登録および参照メカニズムにおけるデータ競合が効果的に解消され、並行環境下での安定性と信頼性が向上しました。
関連リンク
- Go Issue #4214: https://github.com/golang/go/issues/4214
- Go CL 6637047: https://golang.org/cl/6637047 (このコミットに対応するGoの変更リスト)
参考にした情報源リンク
- Go言語の
sync
パッケージドキュメント: https://pkg.go.dev/sync - Go言語の
encoding/gob
パッケージドキュメント: https://pkg.go.dev/encoding/gob - Go言語のメモリモデル: https://go.dev/ref/mem
- Go言語におけるデータ競合の検出 (The Go Programming Language Specification - The Go Memory Model): https://go.dev/ref/spec#Go_memory_model
- Go言語の
reflect
パッケージドキュメント: https://pkg.go.dev/reflect
[インデックス 14092] ファイルの概要
このコミットは、Go言語の標準ライブラリである encoding/gob
パッケージにおけるデータ競合(data race)の修正を目的としています。具体的には、Register
関数および関連する型登録メカニズムにおいて、並行アクセス時に発生しうる競合状態を解消するために、読み書きロック(sync.RWMutex
)が導入されました。
コミット
commit e855fcc3073ccb4dc3fa7a3b7dc2b076e5bb54cf
Author: Rob Pike <r@golang.org>
Date: Tue Oct 9 11:56:38 2012 +1100
encoding/gob: fix data race in Register
Fixes #4214.
R=golang-dev, dsymonds, bradfitz
CC=golang-dev
https://golang.org/cl/6637047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e855fcc3073ccb4dc3fa7a3b7dc2b076e5bb54cf
元コミット内容
encoding/gob: fix data race in Register
Fixes #4214.
R=golang-dev, dsymonds, bradfitz
CC=golang-dev
https://golang.org/cl/6637047
変更の背景
encoding/gob
パッケージは、Goのデータ構造をシリアライズおよびデシリアライズするためのメカニズムを提供します。このパッケージでは、インターフェース型や特定の具象型をエンコード/デコードするために、内部的に型情報を登録・管理する仕組みがあります。この型情報の登録や参照は、Register
関数や関連する内部マップ(nameToConcreteType
、concreteTypeToName
)を通じて行われます。
元の実装では、これらの型情報を管理するマップへのアクセスが同期されていませんでした。そのため、複数のゴルーチンが同時に型を登録しようとしたり、登録された型情報を参照しようとしたりすると、データ競合が発生する可能性がありました。データ競合は、プログラムの予測不能な動作、クラッシュ、または誤ったデータの読み取りにつながる深刻なバグです。
この問題は、GoのIssueトラッカーで #4214 として報告されていました。このコミットは、その報告されたデータ競合を修正することを目的としています。
前提知識の解説
Go言語におけるデータ競合 (Data Race)
データ競合は、複数のゴルーチンが同時に同じメモリ位置にアクセスし、そのうち少なくとも1つのアクセスが書き込みである場合に発生します。さらに、これらのアクセスが同期メカニズムによって順序付けされていない場合にデータ競合とみなされます。データ競合はGoのメモリモデルにおいて未定義の動作を引き起こし、プログラムの信頼性を著しく損ないます。
sync
パッケージと同期プリミティブ
Go言語は、並行処理におけるデータ競合を防ぐために、標準ライブラリの sync
パッケージに様々な同期プリミティブを提供しています。
sync.Mutex
: 排他ロック(Mutual Exclusion Lock)を提供します。Lock()
とUnlock()
メソッドを持ち、一度に1つのゴルーチンのみが保護されたセクションにアクセスできるようにします。読み書き両方の操作を排他的に保護する場合に適しています。sync.RWMutex
: 読み書きロック(Reader-Writer Mutex)を提供します。これは、読み取り操作が頻繁に行われ、書き込み操作が比較的少ない場合に効率的です。RLock()
/RUnlock()
: 読み取りロックを取得/解放します。複数のゴルーチンが同時に読み取りロックを取得できます。Lock()
/Unlock()
: 書き込みロックを取得/解放します。書き込みロックが取得されている間は、他のすべての読み取りロックおよび書き込みロックはブロックされます。
encoding/gob
パッケージ
encoding/gob
パッケージは、Goのプログラム間でGoの値をエンコード(シリアライズ)およびデコード(デシリアライズ)するためのバイナリ形式を提供します。これは、RPC(Remote Procedure Call)や永続化などの用途でよく使用されます。gob
は、エンコード/デコードするデータの型情報を自動的に交換し、必要に応じて型を登録する機能を持っています。
技術的詳細
このコミットの主要な変更点は、encoding/gob
パッケージ内で型情報を管理するグローバルマップ nameToConcreteType
と concreteTypeToName
へのアクセスを sync.RWMutex
を使用して同期することです。
nameToConcreteType
: 型の名前(文字列)からreflect.Type
へのマッピングを保持します。concreteTypeToName
:reflect.Type
から型名(文字列)へのマッピングを保持します。
これらのマップは、gob
がインターフェース型をエンコード/デコードする際に、具象型とそれに対応する名前を解決するために使用されます。
sync.RWMutex
の選択理由
sync.RWMutex
が選択されたのは、これらのマップへのアクセスパターンが「読み取りが頻繁で、書き込み(登録)が比較的少ない」という特性を持つためです。
- 読み取り操作:
decode.go
のdecodeInterface
関数やencode.go
のencodeInterface
関数、type_test.go
のテストコードなど、既存の型情報を参照する際にはRLock()
とRUnlock()
を使用します。これにより、複数のゴルーチンが同時に型情報を安全に読み取ることができます。 - 書き込み操作:
type.go
のRegisterName
関数(およびRegister
関数から呼ばれる)のように、新しい型を登録する際にはLock()
とUnlock()
を使用します。これにより、型情報の変更中に他の読み取りや書き込みがブロックされ、マップの一貫性が保たれます。
sync.Mutex
を使用することも可能ですが、その場合、読み取り操作であっても排他ロックが必要となり、並行性が低下します。sync.RWMutex
を使用することで、読み取り操作の並行性を維持しつつ、書き込み操作の安全性を確保できるため、より効率的な解決策となります。
コアとなるコードの変更箇所
以下のファイルが変更されています。
src/pkg/encoding/gob/decode.go
:decodeInterface
関数内でnameToConcreteType
マップを読み取る際に、registerLock.RLock()
とregisterLock.RUnlock()
が追加されました。
src/pkg/encoding/gob/encode.go
:encodeInterface
関数内でconcreteTypeToName
マップを読み取る際に、registerLock.RLock()
とregisterLock.RUnlock()
が追加されました。
src/pkg/encoding/gob/type.go
:registerLock sync.RWMutex
がグローバル変数として追加されました。RegisterName
関数内で型を登録する際に、registerLock.Lock()
とdefer registerLock.Unlock()
が追加されました。
src/pkg/encoding/gob/type_test.go
:TestRegistrationNaming
テスト関数内でnameToConcreteType
マップを読み取る際に、registerLock.RLock()
とregisterLock.RUnlock()
が追加されました。これは、テスト自体がデータ競合を引き起こさないようにするため、またはテストが実際の使用状況をより正確に反映するようにするための変更です。
コアとなるコードの解説
src/pkg/encoding/gob/type.go
の変更
var (
registerLock sync.RWMutex // 追加
nameToConcreteType = make(map[string]reflect.Type)
concreteTypeToName = make(map[reflect.Type]string)
)
func RegisterName(name string, value interface{}) {
if name == "" {
// reserved for nil
panic("attempt to register empty name")
}
registerLock.Lock() // 追加
defer registerLock.Unlock() // 追加
ut := userType(reflect.TypeOf(value))
// ... (既存のロジック)
}
registerLock sync.RWMutex
:nameToConcreteType
とconcreteTypeToName
マップへのアクセスを保護するための読み書きロックが宣言されました。registerLock.Lock()
とdefer registerLock.Unlock()
:RegisterName
関数は、新しい型を登録する際にこれらのマップを書き換えます。そのため、書き込みロックを取得し、関数が終了する際に確実に解放するようにdefer
を使用しています。これにより、型登録中のマップへの排他的アクセスが保証され、他のゴルーチンからの同時書き込みや読み取りがブロックされます。
src/pkg/encoding/gob/decode.go
の変更
func (dec *Decoder) decodeInterface(ityp reflect.Type, state *decoderState, p uint64) (reflect.Value, error) {
// ... (既存のロジック)
// The concrete type must be registered.
registerLock.RLock() // 追加
typ, ok := nameToConcreteType[name]
registerLock.RUnlock() // 追加
if !ok {
errorf("name not registered for interface: %q", name)
}
// ... (既存のロジック)
}
registerLock.RLock()
とregisterLock.RUnlock()
:decodeInterface
関数は、受信したデータから型名を取得し、それに対応する具象型をnameToConcreteType
マップから検索します。これは読み取り操作であるため、読み取りロックを取得し、検索後に解放します。これにより、複数のデコーダが同時に型情報を読み取ることが可能になります。
src/pkg/encoding/gob/encode.go
の変更
func (enc *Encoder) encodeInterface(b *bytes.Buffer, iv reflect.Value) {
// ... (既存のロジック)
ut := userType(iv.Elem().Type())
registerLock.RLock() // 追加
name, ok := concreteTypeToName[ut.base]
registerLock.RUnlock() // 追加
if !ok {
errorf("type not registered for interface: %s", ut.base)
}
// ... (既存のロジック)
}
registerLock.RLock()
とregisterLock.RUnlock()
:encodeInterface
関数は、エンコードするインターフェースの具象型に対応する名前をconcreteTypeToName
マップから検索します。これも読み取り操作であるため、読み取りロックを使用します。
これらの変更により、encoding/gob
パッケージの型登録および参照メカニズムにおけるデータ競合が効果的に解消され、並行環境下での安定性と信頼性が向上しました。
関連リンク
- Go Issue #4214: https://github.com/golang/go/issues/4214
- Go CL 6637047: https://golang.org/cl/6637047 (このコミットに対応するGoの変更リスト)
参考にした情報源リンク
- Go言語の
sync
パッケージドキュメント: https://pkg.go.dev/sync - Go言語の
encoding/gob
パッケージドキュメント: https://pkg.go.dev/encoding/gob - Go言語のメモリモデル: https://go.dev/ref/mem
- Go言語におけるデータ競合の検出 (The Go Programming Language Specification - The Go Memory Model): https://go.dev/ref/spec#Go_memory_model
- Go言語の
reflect
パッケージドキュメント: https://pkg.go.dev/reflect