[インデックス 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
関数内で行われています。
-
wg.Done()
の移動:- 元のコードでは、
go func(url string) { ... }
の匿名関数内でhttp.Get(url)
の直後にwg.Done()
が記述されていました。 - 変更後のコードでは、
wg.Done()
がdefer
キーワードと共に匿名関数の冒頭に移動されました。 - これにより、このゴルーチンがどのような経路で終了しても(正常終了、エラー、パニックなど)、
wg.Done()
が確実に呼び出されるようになります。
- 元のコードでは、
-
コメントの追加:
defer wg.Done()
の行に「// Decrement the counter when the goroutine completes.
」というコメントが追加され、defer
の意図が明確に示されています。
-
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言語における並行処理のベストプラクティスに関する一般的な情報