[インデックス 18351] ファイルの概要
このコミットは、Go言語の標準ライブラリ sync.Pool
の挙動を、Goのデータ競合検出器 (race detector) が有効な場合に調整するものです。具体的には、sync.Pool
がデータ競合検出器の下で「no-op」(何もしない)に近くなるように変更し、それによって潜在的な競合状態をより効果的に検出できるようにします。
コミット
commit ce8045f3936a957f32755a62235ca71a3cdbff9c
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Sat Jan 25 20:11:16 2014 +0400
sync: support Pool under race detector
Fixes #7203.
R=golang-codereviews, bradfitz
CC=golang-codereviews
https://golang.org/cl/53020044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ce8045f3936a957f32755a62235ca71a3cdbff9c
元コミット内容
sync: support Pool under race detector
Fixes #7203.
R=golang-codereviews, bradfitz
CC=golang-codereviews
https://golang.org/cl/53020044
変更の背景
Goの sync.Pool
は、オブジェクトの再利用を促進し、ガベージコレクションの負荷を軽減することでパフォーマンスを最適化するためのデータ構造です。しかし、sync.Pool
を誤って使用すると、オブジェクトがプールから取得された後やプールに戻された後に適切にリセットされなかったり、複数のゴルーチンによって同時に変更されたりすることで、データ競合(data race)が発生する可能性があります。
Goのデータ競合検出器は、並行処理におけるこのような潜在的なバグを特定するための強力なツールです。しかし、sync.Pool
の内部的な動作(オブジェクトのキャッシュと再利用)は、競合検出器が本来検出したい競合状態を「隠蔽」してしまう可能性がありました。つまり、プールがオブジェクトを再利用することで、本来であれば競合として検出されるはずのメモリアクセスが、異なるゴルーチン間で直接共有される機会が減り、検出器がその競合を見逃す可能性があったのです。
このコミットの背景には、Issue #7203 があります。このIssueでは、sync.Pool
がデータ競合検出器の下で適切に動作しない、あるいは競合を隠蔽してしまう問題が指摘されていました。この変更は、sync.Pool
がデータ競合検出器と共存し、むしろ競合検出を助けるようにその挙動を調整することを目的としています。
前提知識の解説
sync.Pool
sync.Pool
は、一時的なオブジェクトを効率的に再利用するためのGoの標準ライブラリです。主な目的は、頻繁に生成・破棄されるオブジェクト(例: バイトスライス、バッファなど)のガベージコレクションのオーバーヘッドを削減することです。
sync.Pool
は Put
メソッドでオブジェクトをプールに戻し、Get
メソッドでプールからオブジェクトを取得します。プールが空の場合、New
フィールドに設定された関数が呼び出され、新しいオブジェクトが生成されます。
Goのデータ競合検出器 (Race Detector)
Goのデータ競合検出器は、Goプログラムにおける並行処理のバグ、特にデータ競合を検出するためのツールです。データ競合は、少なくとも1つの書き込み操作を含む2つ以上のメモリアクセスが、異なるゴルーチンによって同時に実行され、かつそれらのアクセスが同期メカニズムによって順序付けされていない場合に発生します。データ競合はプログラムの非決定的な動作やクラッシュを引き起こす可能性があります。
データ競合検出器は、プログラムの実行時にメモリアクセスを監視し、競合のパターンを検出します。Goプログラムを -race
フラグを付けてビルドまたは実行することで有効にできます(例: go run -race main.go
または go build -race -o myapp && ./myapp
)。競合検出器は、実行速度を低下させ、メモリ使用量を増加させますが、並行処理のバグを見つける上で非常に効果的です。
「no-op」 (No Operation)
「no-op」とは、「何もしない操作」を意味します。この文脈では、sync.Pool
が通常行うオブジェクトのキャッシュや再利用といった最適化の動作を停止し、単にオブジェクトを生成したり、すぐに破棄したりするだけの挙動になることを指します。
技術的詳細
このコミットの核心は、sync.Pool
の Put
および Get
メソッドに raceenabled
というグローバル変数をチェックする条件分岐を追加することです。raceenabled
は、Goのデータ競合検出器が有効になっているかどうかを示すブール値です。
-
Put
メソッドの変更:raceenabled
がtrue
の場合、Put
メソッドは受け取ったオブジェクトをプールに格納せず、単にreturn
します。これにより、オブジェクトはプールされずにガベージコレクションの対象となります。 コメントには「Under race detector the Pool degenerates into no-op. It's conforming, simple and does not introduce excessive happens-before edges between unrelated goroutines.」とあります。これは、競合検出器が有効な場合、sync.Pool
は「no-op」に退化し、無関係なゴルーチン間で過度な happens-before 関係を導入しないため、競合検出が容易になることを意味します。 -
Get
メソッドの変更:raceenabled
がtrue
の場合、Get
メソッドはプールから既存のオブジェクトを取得しようとせず、代わりにp.New
関数が設定されていればそれを呼び出して新しいオブジェクトを生成して返します。p.New
がnil
の場合はnil
を返します。 これにより、Get
は常に新しいオブジェクトを生成するか、nil
を返すようになり、プールからの再利用は行われません。 -
テストファイルの変更:
sync/pool_test.go
にビルドタグ+build !race
が追加されました。これは、このテストファイルがデータ競合検出器が有効なビルドではコンパイルされないことを意味します。その理由として「Pool is no-op under race detector, so all these tests do not work.」とコメントされています。つまり、sync.Pool
がno-opになるため、プールの効率性や再利用を検証する既存のテストは、競合検出器が有効な環境では意味をなさなくなるため、実行をスキップするようになっています。
この変更の意図は、sync.Pool
がオブジェクトを再利用することで、本来検出されるべきデータ競合を隠蔽する可能性を排除することです。競合検出器が有効な場合、sync.Pool
は事実上無効化され、オブジェクトは常に新しく割り当てられるか、nil
が返されます。これにより、オブジェクトのライフサイクルが単純化され、共有状態へのアクセスがより直接的になり、競合検出器が潜在的な競合をより確実に特定できるようになります。これは、パフォーマンス最適化よりもバグ検出を優先するという、デバッグツールの設計思想に基づいています。
コアとなるコードの変更箇所
src/pkg/sync/pool.go
--- a/src/pkg/sync/pool.go
+++ b/src/pkg/sync/pool.go
@@ -72,6 +72,12 @@ func init() {
// Put adds x to the pool.
func (p *Pool) Put(x interface{}) {
+ if raceenabled {
+ // Under race detector the Pool degenerates into no-op.
+ // It's conforming, simple and does not introduce excessive
+ // happens-before edges between unrelated goroutines.
+ return
+ }
if x == nil {
return
}
@@ -95,6 +101,12 @@ func (p *Pool) Put(x interface{}) {
// If Get would otherwise return nil and p.New is non-nil, Get returns
// the result of calling p.New.
func (p *Pool) Get() interface{} {
+ if raceenabled {
+ if p.New != nil {
+ return p.New()
+ }
+ return nil
+ }
l := p.pin()
t := l.tail
if t > 0 {
src/pkg/sync/pool_test.go
--- a/src/pkg/sync/pool_test.go
+++ b/src/pkg/sync/pool_test.go
@@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.\n
+// Pool is no-op under race detector, so all these tests do not work.
+// +build !race
+
package sync_test
import (
コアとなるコードの解説
src/pkg/sync/pool.go
の変更
-
Put
メソッド:if raceenabled { return }
という行が追加されました。raceenabled
はGoランタイムによって設定される内部変数で、データ競合検出器が有効な場合にtrue
になります。この条件が真の場合、Put
メソッドは即座にリターンし、オブジェクトをプールに格納する通常のロジックは実行されません。これにより、Put
は実質的に何もしない(no-op)になります。 -
Get
メソッド:if raceenabled { ... }
というブロックが追加されました。このブロック内では、p.New
フィールド(新しいオブジェクトを生成するための関数)がnil
でない場合、p.New()
を呼び出して新しいオブジェクトを生成し、それを返します。p.New
がnil
の場合はnil
を返します。これにより、Get
はプールから既存のオブジェクトを再利用するのではなく、常に新しいオブジェクトを生成するか、何も返さないようになります。
これらの変更により、データ競合検出器が有効な環境では、sync.Pool
はオブジェクトの再利用という本来の目的を果たさなくなります。これは意図的な挙動であり、オブジェクトの再利用によって隠蔽されがちなデータ競合を、検出器がより容易に特定できるようにするためです。
src/pkg/sync/pool_test.go
の変更
- ファイルの先頭に
// +build !race
というビルドタグが追加されました。 このタグは、Goのビルドシステムに対する指示です。!race
は「raceタグが設定されていない場合」を意味します。つまり、このテストファイルは、go test -race
のように-race
フラグを付けてビルドされる際にはコンパイルされず、通常のビルドでのみコンパイル・実行されます。 これにより、sync.Pool
がno-opになる環境下で、プールの機能性を検証するテストが誤って失敗するのを防ぎます。
関連リンク
- Go Issue #7203: https://github.com/golang/go/issues/7203
- Go Race Detector Documentation: https://go.dev/doc/articles/race_detector
sync.Pool
Documentation: https://pkg.go.dev/sync#Pool
参考にした情報源リンク
- Go Issue #7203: https://github.com/golang/go/issues/7203
- Go Race Detector Documentation: https://go.dev/doc/articles/race_detector
sync.Pool
Documentation: https://pkg.go.dev/sync#Pool- Web search results for "Go sync.Pool race detector behavior"
- Web search results for "Go issue 7203"