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

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

このコミットは、Go言語のランタイムパッケージ内の並列処理テスト (parfor_test.go) におけるデッドロックの問題を修正するものです。具体的には、テスト中にガベージコレクション (GC) が要求された際に発生するデッドロックを解消します。

コミット

commit 6ae448e8dfc675c0fbda18e2b555af54ae656f69
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Tue Nov 6 20:11:16 2012 +0400

    runtime: fix deadlock in parallel for test
    The deadlock occurs when another goroutine requests GC
    during the test. When wait=true the test expects physical parallelism,
    that is, that P goroutines are all active at the same time.
    If GC is requested, then part of the goroutines are not scheduled,
    so other goroutines deadlock.
    With wait=false, goroutines finish parallel for w/o waiting for all
    other goroutines.
    Fixes #3954.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/6820098

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

https://github.com/golang/go/commit/6ae448e8dfc675c0fbda18e2b555af54ae656f69

元コミット内容

このコミットは、Goランタイムの並列処理テスト (TestParForParallel) において発生するデッドロックを修正します。デッドロックは、テストの実行中に別のゴルーチンがガベージコレクション (GC) を要求した際に発生していました。

元のテストでは、ParForSetup 関数の wait パラメータが true に設定されており、これはテストが物理的な並列性、つまり P 個のゴルーチンがすべて同時にアクティブであることを期待していることを意味します。しかし、GCが要求されると、一部のゴルーチンがスケジューリングされなくなり、その結果、他のゴルーチンが完了を待つことになりデッドロックが発生していました。

この修正は、wait パラメータを false に変更することで、ゴルーチンが他のすべてのゴルーチンを待たずに並列処理を終了できるようにし、デッドロックを回避します。

変更の背景

この変更の背景には、Goランタイムの並列処理テストの信頼性の問題がありました。具体的には、Issue 3954として報告されたデッドロックが原因です。

Goのランタイムは、複数のCPUコアを効率的に利用するためにゴルーチンとスケジューラを管理します。GOMAXPROCS は、同時に実行できるOSスレッドの最大数を設定し、Goスケジューラがその数だけゴルーチンをOSスレッドにマッピングします。ParFor のような並列処理テストは、この並列実行のメカニズムを検証するために設計されています。

しかし、テスト中にガベージコレクション (GC) が発生すると、GCは一時的にすべてのゴルーチンの実行を停止させたり、一部のゴルーチンのスケジューリングを遅延させたりすることがあります。元のテストでは、wait=true の設定により、すべての並列ゴルーチンが同時に完了することを期待していました。GCによるスケジューリングの遅延や停止が発生すると、一部のゴルーチンが期待通りに進行せず、他のゴルーチンがそれらの完了を無限に待ち続ける状態に陥り、結果としてデッドロックが発生していました。

このデッドロックは、テストが不安定になる原因であり、ランタイムの並列処理の正確な動作を検証する上で障害となっていました。そのため、テストの信頼性を向上させ、ランタイムの並列処理がGCの影響下でも正しく動作することを確認するために、この修正が必要とされました。

前提知識の解説

  • Go言語の並列処理: Go言語はゴルーチン (goroutine) とチャネル (channel) を用いた並行処理を特徴としています。ゴルーチンは軽量なスレッドのようなもので、Goランタイムのスケジューラによって管理されます。
  • GOMAXPROCS: Goプログラムが同時に実行できるOSスレッドの最大数を設定する環境変数または関数です。デフォルトではCPUコア数に設定されます。この値は、GoスケジューラがゴルーチンをOSスレッドにマッピングする際のヒントとして使用されます。
  • ガベージコレクション (GC): Goランタイムは自動的にメモリ管理を行います。不要になったメモリ領域を自動的に解放するプロセスがGCです。GoのGCは、一般的に「Stop-the-World (STW)」フェーズを持ち、この間はすべてのゴルーチンの実行が一時的に停止されます。これにより、GCがメモリの状態を安全に検査・変更できます。STWフェーズは短時間ですが、並列処理のタイミングに影響を与える可能性があります。
  • デッドロック: 複数のプロセスやスレッドが、互いに相手が保持しているリソースの解放を待ち続け、結果としてどのプロセスも処理を進められなくなる状態です。並行プログラミングにおいてよく発生する問題の一つです。
  • ParFor (Parallel For): Goランタイムのテストコード内で使用される、並列ループ処理をシミュレートするためのユーティリティです。複数のゴルーチンに処理を分散させ、並列に実行されることを想定しています。ParForSetup で並列処理の設定を行い、ParForDo で実際に並列処理を実行します。
  • ParForSetupwait パラメータ: ParForSetup 関数には wait というブール型のパラメータがあります。これが true の場合、ParForDo を呼び出すゴルーチンは、すべての並列ゴルーチンが完了するまで待機します。false の場合、呼び出し元のゴルーチンは他の並列ゴルーチンの完了を待たずに処理を進めます。

技術的詳細

この修正は、src/pkg/runtime/parfor_test.go 内の TestParForParallel 関数に焦点を当てています。このテストは、ParFor ユーティリティを使用して、複数のゴルーチンが並列にデータ処理を行うシナリオをシミュレートします。

元の実装では、ParForSetup の第6引数(wait パラメータ)が true に設定されていました。これは、ParForDo を呼び出すゴルーチンが、他のすべての並列ゴルーチンが処理を完了するまでブロックすることを意味します。この「物理的な並列性」を期待する設計が、GCの介入によって問題を引き起こしました。

GCが実行されると、Goスケジューラは一時的にゴルーチンの実行を停止または遅延させます。これにより、一部のゴルーチンが期待通りに進行せず、wait=true の設定によって他のゴルーチンがそれらの完了を無限に待ち続ける状態に陥り、デッドロックが発生していました。

修正では、以下の2つの主要な変更が行われました。

  1. ParForSetupwait パラメータを false に変更: これにより、ParForDo を呼び出すゴルーチンは、他の並列ゴルーチンの完了を待たずに自身の処理を終了できるようになります。これにより、GCによるスケジューリングの遅延があっても、テスト全体がデッドロックに陥ることを防ぎます。
  2. チャネルによる明示的な同期の導入: wait=false に変更したことで、各ゴルーチンが独立して終了するようになるため、テストのメインゴルーチンがすべての並列処理の完了を待つための新しいメカニズムが必要になりました。これを実現するために、バッファ付きチャネル c が導入されました。
    • c := make(chan bool, P): PGOMAXPROCS の値であり、同時に実行されるゴルーチンの最大数を示します。このチャネルは、P 個のゴルーチンが完了したことを通知するために使用されます。
    • 各並列ゴルーチンは、ParForDo(desc) の実行が完了した後に c <- true を実行してチャネルに値を送信します。
    • メインゴルーチンは、for p := 1; p < P; p++ { <-c } ループを使用して、P-1 個の並列ゴルーチンからの完了通知を待ちます(メインゴルーチン自身も ParForDo を実行するため、P-1 回の受信で十分です)。

この変更により、テストはGCの介入に対してより堅牢になり、デッドロックを回避しながらも、並列処理が正しく完了したことを検証できるようになりました。

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

src/pkg/runtime/parfor_test.go ファイルの TestParForParallel 関数内。

--- a/src/pkg/runtime/parfor_test.go
+++ b/src/pkg/runtime/parfor_test.go
@@ -109,14 +109,21 @@ func TestParForParallel(t *testing.T) {
 		data[i] = i
 	}
 	P := GOMAXPROCS(-1)
+	c := make(chan bool, P)
 	desc := NewParFor(uint32(P))
-	ParForSetup(desc, uint32(P), uint32(N), nil, true, func(desc *ParFor, i uint32) {
+	ParForSetup(desc, uint32(P), uint32(N), nil, false, func(desc *ParFor, i uint32) {
 		data[i] = data[i]*data[i] + 1
 	})
 	for p := 1; p < P; p++ {
-		go ParForDo(desc)
+		go func() {
+			ParForDo(desc)
+			c <- true
+		}()
 	}
 	ParForDo(desc)
+	for p := 1; p < P; p++ {
+		<-c
+	}
 	for i := uint64(0); i < N; i++ {
 		if data[i] != i*i+1 {
 			t.Fatalf("Wrong element %d: %d", i, data[i])

コアとなるコードの解説

  1. P := GOMAXPROCS(-1):
    • GOMAXPROCS(-1) は現在の GOMAXPROCS の値を取得します。この値は、Goランタイムが同時に実行できるOSスレッドの最大数を示し、並列処理の度合いを決定します。
  2. c := make(chan bool, P):
    • P の容量を持つバッファ付きチャネル c を作成します。このチャネルは、並列実行されるゴルーチンが完了したことをメインゴルーチンに通知するために使用されます。バッファ付きチャネルであるため、送信側は受信側が準備できるまでブロックされません(バッファが満杯でない限り)。
  3. ParForSetup(desc, uint32(P), uint32(N), nil, false, func(desc *ParFor, i uint32) { ... }):
    • ParFor の設定を行います。ここで最も重要な変更は、第6引数の wait パラメータが true から false に変更された点です。
    • wait: false は、ParForDo を呼び出すゴルーチンが、他の並列ゴルーチンの完了を待たずに自身の処理を終了することを意味します。これにより、GCによるスケジューリングの遅延があっても、個々のゴルーチンがブロックされ続けることを防ぎます。
    • 匿名関数 func(desc *ParFor, i uint32) { data[i] = data[i]*data[i] + 1 } は、各並列ゴルーチンが実行する処理(ここでは data 配列の要素を更新する)を定義しています。
  4. for p := 1; p < P; p++ { go func() { ParForDo(desc); c <- true }() }:
    • P-1 個の新しいゴルーチンを起動します(メインゴルーチンも ParForDo を実行するため)。
    • 各ゴルーチンは匿名関数を実行します。この匿名関数内で ParForDo(desc) が呼び出され、並列処理が実行されます。
    • ParForDo(desc) が完了した後、c <- true を実行してチャネル c に値を送信します。これは、このゴルーチンが自身の並列処理を完了したことを示すシグナルです。
  5. ParForDo(desc):
    • メインゴルーチン自身も ParForDo を呼び出し、並列処理の一部を実行します。
  6. for p := 1; p < P; p++ { <-c }:
    • メインゴルーチンは、このループでチャネル c から P-1 回値を受信します。
    • これにより、メインゴルーチンは、自身以外のすべての並列ゴルーチンが ParForDo の実行を完了し、チャネルにシグナルを送信したことを確認できます。これは、wait=false に変更したことによる明示的な同期メカニズムです。
  7. for i := uint64(0); i < N; i++ { ... }:
    • すべての並列処理が完了した後、data 配列の各要素が期待通りの値になっているかを確認し、テストの成功/失敗を判断します。

この変更により、テストはGCの介入に対してより堅牢になり、デッドロックを回避しながらも、並列処理が正しく完了したことを検証できるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (ゴルーチン、チャネル、GCに関する一般的な情報)
  • Goのソースコード (src/pkg/runtime/parfor_test.go の変更履歴と関連ファイル)
  • Go Issue Tracker (Issue 3954の詳細)
  • Goの変更リスト (CL 6820098の詳細)
  • Goの並行処理に関する一般的な解説記事