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

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

このコミットは、Go言語のランタイムにおけるマップ(map)の並行読み取りに関するテストを追加するものです。具体的には、マップが成長する過程で並行して読み取りが行われた場合に発生する可能性のある問題を検出するためのテストケースが導入されています。ただし、このテストはコミット時点では既知のクラッシュを引き起こすため、一時的に無効化されています。

コミット

commit d76f28fc39400b918f7951b7e7f12b0dc4a98b0a
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Mon Apr 1 11:49:24 2013 -0700

    runtime: add concurrent map read test
    
    Currently crashes, so disabled.
    
    Update #5179
    
    R=golang-dev, khr
    CC=golang-dev
    https://golang.org/cl/8222044

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

https://github.com/golang/go/commit/d76f28fc39400b918f7951b7e7f12b0dc4a98b0a

元コミット内容

このコミットの目的は、Goランタイムのマップ実装における並行読み取りのテストを追加することです。コミットメッセージには「Currently crashes, so disabled. (現在クラッシュするため、無効化されています。)」と明記されており、このテストが既存のバグを露呈させるものであることが示唆されています。また、「Update #5179」という記述から、golang.org/issue/5179という既存のイシューに関連する変更であることがわかります。

変更の背景

このコミットの背景には、Go言語のランタイムにおけるハッシュマップ(map型)の新しい実装が関係しています。当時のGoランタイムのハッシュマップは、読み取り操作中に遅延的に(lazily)成長する(要素が追加されて容量が足りなくなった場合に内部的にサイズを増やす)設計になっていました。しかし、複数のゴルーチン(goroutine)が同時にマップを読み取っている最中に、そのマップが成長しようとすると、内部データ構造が破損し、結果としてプログラムがクラッシュするという問題が発見されました。

特に、GOMAXPROCS環境変数が1より大きい場合(つまり、複数のCPUコアが利用可能な並行実行環境)にこの問題が顕著に発生しました。これは、マップの成長処理が並行して安全に行われるように設計されていなかったためです。この問題はgolang.org/issue/5179として報告され、このコミットはその問題の存在を確認し、将来的な修正のためにテストケースを追加する目的で作成されました。テストは問題が修正されるまで一時的にスキップされる形になっています。

前提知識の解説

  • Go言語のマップ (map): Go言語におけるマップは、キーと値のペアを格納するための組み込みデータ型です。内部的にはハッシュテーブルとして実装されており、高速なキーによる値の検索、追加、削除が可能です。
  • 並行性 (Concurrency): Go言語はゴルーチン(goroutine)とチャネル(channel)を用いた並行プログラミングを強力にサポートしています。複数のゴルーチンが同時に実行されることで、プログラムのパフォーマンスを向上させることができます。
  • ハッシュテーブルの成長 (Hash Table Growth): ハッシュテーブルは、格納される要素数が増えるにつれて、内部的な容量を増やす(リサイズする)必要があります。このリサイズ処理は、既存の要素を新しい、より大きな内部配列に再配置する作業を伴います。
  • データ競合 (Data Race): 複数のゴルーチンが同じメモリ領域に同時にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生する問題です。データ競合が発生すると、プログラムの動作が予測不能になったり、クラッシュしたりする可能性があります。Go言語では、syncパッケージのミューテックス(sync.Mutex)などを用いてデータ競合を防ぐのが一般的です。
  • GOMAXPROCS: Goランタイムが同時に実行できるOSスレッドの最大数を制御する環境変数です。デフォルトではCPUのコア数に設定されます。GOMAXPROCS > 1の場合、複数のゴルーチンが真に並行して実行される可能性が高まります。
  • t.Skip(): Goのテストフレームワーク(testingパッケージ)で提供される関数で、テストをスキップするために使用されます。テストがまだ開発中であるか、既知のバグのために一時的に無効化する必要がある場合などに利用されます。

技術的詳細

このコミットで追加されたTestConcurrentReadsAfterGrowthテスト関数は、Goランタイムのマップが成長する過程で並行読み取りが行われた際の挙動を検証することを目的としています。

テストのロジックは以下の通りです。

  1. t.Skip("Known currently broken; golang.org/issue/5179"): この行により、テストは常にスキップされます。これは、テストが意図的にクラッシュを引き起こすため、問題が修正されるまで実行しないようにするための措置です。
  2. GOMAXPROCSの設定: os.Getenv("GOMAXPROCS") == ""の場合、runtime.GOMAXPROCS(16)を設定しています。これは、テストが複数のCPUコアを最大限に活用する環境で実行されることを保証し、並行性の問題を再現しやすくするためです。テスト終了後には元のGOMAXPROCS値に戻すdeferが設定されています。
  3. テストパラメータ:
    • numLoop: 外側のループの回数(デフォルト10回、testing.Short()の場合は2回)。
    • numGrowStep: マップを成長させるステップ数(デフォルト250回、testing.Short()の場合は500回)。
    • numReader: 並行してマップを読み取るゴルーチンの数(16個)。
  4. マップの初期化と成長:
    • 外側のループで新しいマップmが作成されます。
    • 内側のnumGrowStepループでは、マップに新しいキーと値のペア(m[gs] = gs)が追加され、マップが徐々に成長させられます。
  5. 並行読み取りのシミュレーション:
    • マップが成長する各ステップで、numReader個のゴルーチンが2つの異なる方法でマップを並行して読み取ります。
    • 最初のゴルーチン群: for _ = range m {} を使用してマップ全体をイテレートします。これはマップの内部構造を走査するため、成長中のマップの破損を検出しやすいです。
    • 二番目のゴルーチン群: for key := 0; key < gs; key++ { _ = m[key] } を使用して、マップに既に追加されたキーを個別にルックアップします。これもまた、マップの内部状態の一貫性を検証するのに役立ちます。
    • sync.WaitGroupを使用して、すべての読み取りゴルーチンが完了するまで待機します。

このテストの設計は、マップの成長と並行読み取りが同時に発生するシナリオを意図的に作り出し、その際にランタイムがクラッシュするかどうかを検証するものです。当時のGoランタイムのマップ実装では、このシナリオでデータ競合が発生し、内部構造が破損してクラッシュに至っていました。

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

変更はsrc/pkg/runtime/map_test.goファイルに集中しています。

--- a/src/pkg/runtime/map_test.go
+++ b/src/pkg/runtime/map_test.go
@@ -7,8 +7,10 @@ package runtime_test
 import (
 	"fmt"
 	"math"
+	"os"
 	"runtime"
 	"sort"
+	"sync"
 	"testing"
 )
 
@@ -231,6 +233,43 @@ func TestIterGrowWithGC(t *testing.T) {
 	}
 }
 
+func TestConcurrentReadsAfterGrowth(t *testing.T) {
+	// TODO(khr): fix and enable this test.
+	t.Skip("Known currently broken; golang.org/issue/5179")
+
+	if os.Getenv("GOMAXPROCS") == "" {
+		defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(16))
+	}
+	numLoop := 10
+	numGrowStep := 250
+	numReader := 16
+	if testing.Short() {
+		numLoop, numGrowStep = 2, 500
+	}
+	for i := 0; i < numLoop; i++ {
+		m := make(map[int]int, 0)
+		for gs := 0; gs < numGrowStep; gs++ {
+			m[gs] = gs
+			var wg sync.WaitGroup
+			wg.Add(numReader * 2)
+			for nr := 0; nr < numReader; nr++ {
+				go func() {
+					defer wg.Done()
+					for _ = range m {
+					}
+				}()
+				go func() {
+					defer wg.Done()
+					for key := 0; key < gs; key++ {
+						_ = m[key]
+					}
+				}()
+			}
+			wg.Wait()
+		}
+	}
+}
+
 func TestBigItems(t *testing.T) {
 	var key [256]string
 	for i := 0; i < 256; i++ {

コアとなるコードの解説

  • import ("os", "sync"): 新しく追加されたTestConcurrentReadsAfterGrowth関数内でos.GetenvGOMAXPROCS環境変数の読み取りのため)とsync.WaitGroup(ゴルーチンの同期のため)を使用するために、これらのパッケージがインポートされています。
  • func TestConcurrentReadsAfterGrowth(t *testing.T): この関数が追加された新しいテストケースです。
    • t.Skip(...): 前述の通り、既知のバグのためにテストをスキップします。
    • if os.Getenv("GOMAXPROCS") == "" { defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(16)) }: GOMAXPROCSが設定されていない場合、テスト実行中に一時的に16に設定し、テスト終了後に元の値に戻します。これにより、並行実行環境をシミュレートします。
    • numLoop, numGrowStep, numReader: テストの反復回数、マップの成長ステップ数、並行読み取りゴルーチンの数を定義しています。testing.Short()が有効な場合は、テスト時間を短縮するためにこれらの値が調整されます。
    • for i := 0; i < numLoop; i++: 複数のテストサイクルを実行します。各サイクルで新しいマップが作成されます。
    • m := make(map[int]int, 0): 新しい空のマップを作成します。
    • for gs := 0; gs < numGrowStep; gs++: このループでマップに要素を追加し、マップを成長させます。
      • m[gs] = gs: マップに新しいキーと値のペアを追加します。
      • var wg sync.WaitGroup: ゴルーチンの完了を待つためのWaitGroupを宣言します。
      • wg.Add(numReader * 2): numReader個のゴルーチンがそれぞれ2つの読み取り操作を行うため、WaitGroupのカウンタをnumReader * 2に設定します。
      • for nr := 0; nr < numReader; nr++: numReader個の並行読み取りゴルーチンを起動します。
        • go func() { defer wg.Done(); for _ = range m {} }(): マップ全体をイテレートするゴルーチン。
        • go func() { defer wg.Done(); for key := 0; key < gs; key++ { _ = m[key] } }(): マップの特定のキーをルックアップするゴルーチン。
      • wg.Wait(): すべての読み取りゴルーチンが完了するまで待機します。

このコードは、マップの成長と並行読み取りが同時に行われる状況を意図的に作り出し、当時のGoランタイムのマップ実装がこのシナリオでクラッシュするという既知の問題を再現するためのものです。テストがスキップされているのは、この問題が修正されるまで、テストスイート全体の実行を妨げないようにするためです。

関連リンク

参考にした情報源リンク