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

[インデックス 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.PoolPut メソッドでオブジェクトをプールに戻し、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.PoolPut および Get メソッドに raceenabled というグローバル変数をチェックする条件分岐を追加することです。raceenabled は、Goのデータ競合検出器が有効になっているかどうかを示すブール値です。

  1. Put メソッドの変更: raceenabledtrue の場合、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 関係を導入しないため、競合検出が容易になることを意味します。

  2. Get メソッドの変更: raceenabledtrue の場合、Get メソッドはプールから既存のオブジェクトを取得しようとせず、代わりに p.New 関数が設定されていればそれを呼び出して新しいオブジェクトを生成して返します。p.Newnil の場合は nil を返します。 これにより、Get は常に新しいオブジェクトを生成するか、nil を返すようになり、プールからの再利用は行われません。

  3. テストファイルの変更: 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.Newnil の場合は nil を返します。これにより、Get はプールから既存のオブジェクトを再利用するのではなく、常に新しいオブジェクトを生成するか、何も返さないようになります。

これらの変更により、データ競合検出器が有効な環境では、sync.Pool はオブジェクトの再利用という本来の目的を果たさなくなります。これは意図的な挙動であり、オブジェクトの再利用によって隠蔽されがちなデータ競合を、検出器がより容易に特定できるようにするためです。

src/pkg/sync/pool_test.go の変更

  • ファイルの先頭に // +build !race というビルドタグが追加されました。 このタグは、Goのビルドシステムに対する指示です。!race は「raceタグが設定されていない場合」を意味します。つまり、このテストファイルは、go test -race のように -race フラグを付けてビルドされる際にはコンパイルされず、通常のビルドでのみコンパイル・実行されます。 これにより、sync.Pool がno-opになる環境下で、プールの機能性を検証するテストが誤って失敗するのを防ぎます。

関連リンク

参考にした情報源リンク