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

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

このコミットは、Go言語の標準ライブラリ sync パッケージ内の WaitGroup の使用例を改善するものです。具体的には、WaitGroup.Done() メソッドの呼び出しを defer ステートメント内に配置することで、ゴルーチンが異常終了した場合でもカウンタが確実にデクリメントされるようにし、WaitGroup が待機状態のままになる(ハングする)可能性を低減します。

コミット

commit c2fb6e2c0bf7ba6866eb27567b5d16683680e63b
Author: Gaal Yahas <gaal@google.com>
Date:   Thu Feb 7 00:39:52 2013 +0800

    sync: improve WaitGroup example by putting the call to Done in a
    deferred block. This makes hangs in the waiting code less likely
    if a goroutine exits abnormally.
    
    R=golang-dev, minux.ma
    CC=golang-dev
    https://golang.org/cl/7306052

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

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

元コミット内容

sync: improve WaitGroup example by putting the call to Done in a
deferred block. This makes hangs in the waiting code less likely
if a goroutine exits abnormally.

R=golang-dev, minux.ma
CC=golang-dev
https://golang.org/cl/7306052

変更の背景

sync.WaitGroup は、複数のゴルーチンの完了を待機するために使用される同期プリミティブです。通常、WaitGroup.Add(n) でカウンタを増やし、各ゴルーチンが処理を終えるたびに WaitGroup.Done() を呼び出してカウンタを減らします。そして、WaitGroup.Wait() を呼び出すことで、カウンタがゼロになるまで待機します。

しかし、ゴルーチン内でエラーが発生したり、パニック(panic)によって異常終了したりした場合、WaitGroup.Done() が呼び出されない可能性があります。このような状況が発生すると、WaitGroup のカウンタがゼロにならず、WaitGroup.Wait() を呼び出しているコードが永遠に待機し続ける、いわゆる「デッドロック」や「ハング」状態に陥る可能性があります。

このコミットは、このような潜在的な問題を回避するために、WaitGroup.Done() の呼び出しを defer ステートメント内に移動することで、ゴルーチンの実行がどのように終了しても(正常終了、エラーによる終了、パニックによる終了のいずれであっても)、Done() が確実に実行されるようにする改善です。これは、堅牢な並行処理コードを書く上でのベストプラクティスの一つです。

前提知識の解説

Goにおけるゴルーチン (Goroutine)

ゴルーチンは、Go言語における軽量な並行処理の単位です。OSのスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行することが可能です。go キーワードを関数の前に置くことで簡単に起動できます。

go func() {
    // この中で並行処理が実行される
}()

sync.WaitGroup

sync.WaitGroup は、複数のゴルーチンが完了するまでメインゴルーチンが待機するための同期メカニズムです。以下の3つの主要なメソッドを持ちます。

  • Add(delta int): WaitGroup のカウンタを delta だけ増やします。通常、新しいゴルーチンを起動する前に呼び出されます。
  • Done(): WaitGroup のカウンタを1だけ減らします。ゴルーチンがそのタスクを完了したときに呼び出されます。
  • Wait(): WaitGroup のカウンタがゼロになるまでブロックします。

defer ステートメント

defer ステートメントは、そのステートメントを含む関数がリターンする直前に、指定された関数呼び出しを実行することを保証します。これは、リソースの解放(ファイルのクローズ、ロックの解除など)や、今回のケースのように WaitGroup.Done() のようなクリーンアップ処理を確実に行うために非常に有用です。

defer はLIFO(Last-In, First-Out)の順序で実行されます。つまり、複数の defer ステートメントがある場合、最後に宣言された defer が最初に実行されます。

defer の重要な特性は、関数が正常にリターンする場合だけでなく、パニックによって異常終了する場合でも実行される点です。これにより、リソースリークや同期の問題を防ぐことができます。

技術的詳細

このコミットの核心は、WaitGroup.Done() の呼び出しを defer ステートメント内に移動したことです。

変更前は、http.Get(url) の直後に wg.Done() が呼び出されていました。

// Fetch the URL.
http.Get(url)
// Decrement the counter.
wg.Done()

このコードでは、もし http.Get(url) の呼び出し中にパニックが発生したり、何らかの理由でゴルーチンが wg.Done() に到達する前に終了してしまったりした場合、wg.Done() が実行されず、WaitGroup のカウンタが正のまま残ってしまいます。結果として、wg.Wait() を呼び出しているメインゴルーチンは、永遠に待機し続けることになります。

変更後は、wg.Done()defer ステートメント内に配置されました。

// Decrement the counter when the goroutine completes.
defer wg.Done()
// Fetch the URL.
http.Get(url)

defer wg.Done() と記述することで、このゴルーチンが実行されている匿名関数が終了する直前(正常終了、エラーによる終了、パニックによる終了のいずれであっても)に、wg.Done() が必ず呼び出されることが保証されます。これにより、ゴルーチンの途中で予期せぬエラーが発生しても、WaitGroup のカウンタは適切にデクリメントされ、WaitGroup.Wait() がハングするリスクが大幅に低減されます。

これは、並行処理における堅牢性を高めるための重要なパターンであり、Go言語で WaitGroup を使用する際のベストプラクティスとして広く推奨されています。

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

diff --git a/src/pkg/sync/example_test.go b/src/pkg/sync/example_test.go
index 1564924003..031c87f03b 100644
--- a/src/pkg/sync/example_test.go
+++ b/src/pkg/sync/example_test.go
@@ -24,10 +24,10 @@ func ExampleWaitGroup() {
 		wg.Add(1)
 		// Launch a goroutine to fetch the URL.
 		go func(url string) {
+			// Decrement the counter when the goroutine completes.
+			defer wg.Done()
 			// Fetch the URL.
 			http.Get(url)
-			// Decrement the counter.
-			wg.Done()
 		}(url)
 	}
 	// Wait for all HTTP fetches to complete.
@@ -37,7 +37,7 @@ func ExampleWaitGroup() {
 func ExampleOnce() {
 	var once sync.Once
 	onceBody := func() {
-		fmt.Printf("Only once\\n")
+		fmt.Println("Only once")
 	}\n \tdone := make(chan bool)\n \tfor i := 0; i < 10; i++ {\n

コアとなるコードの解説

変更は src/pkg/sync/example_test.go ファイルの ExampleWaitGroup 関数内で行われています。

  1. wg.Done() の移動:

    • 元のコードでは、go func(url string) { ... } の匿名関数内で http.Get(url) の直後に wg.Done() が記述されていました。
    • 変更後のコードでは、wg.Done()defer キーワードと共に匿名関数の冒頭に移動されました。
    • これにより、このゴルーチンがどのような経路で終了しても(正常終了、エラー、パニックなど)、wg.Done() が確実に呼び出されるようになります。
  2. コメントの追加:

    • defer wg.Done() の行に「// Decrement the counter when the goroutine completes.」というコメントが追加され、defer の意図が明確に示されています。
  3. ExampleOnce 関数の変更 (軽微な修正):

    • ExampleOnce 関数では、fmt.Printf("Only once\\n")fmt.Println("Only once") に変更されています。これは機能的な変更ではなく、単に文字列出力の形式をより簡潔なものに修正したものです。このコミットの主要な目的である WaitGroup の改善とは直接関係ありませんが、同じファイル内の軽微なクリーンアップとして含まれています。

このコミットの主要な目的は、WaitGroup の使用例をより堅牢なものにすることであり、defer の適切な使用法を示す良い例となっています。

関連リンク

  • https://golang.org/cl/7306052 (Go Gerrit Code Review)

参考にした情報源リンク

  • Go言語公式ドキュメント: sync パッケージ (WaitGroup の説明を含む)
  • Go言語公式ドキュメント: defer ステートメント
  • Go言語における並行処理のベストプラクティスに関する一般的な情報