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

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

このコミットは、Go言語の標準ライブラリ sync パッケージ内の WaitGroup 型の Add メソッドに関するドキュメンテーションを改善するものです。具体的には、Add メソッドを呼び出すタイミングに関する重要な注意点を追加し、WaitGroup の誤用による潜在的なバグを防ぐことを目的としています。

コミット

commit 02e05817ada2bd3d0492387a032b1aa879c2cd3f
Author: Russ Cox <rsc@golang.org>
Date:   Mon Feb 11 08:05:46 2013 -0500

    sync: add caution about where to call (*WaitGroup).Add
    
    Fixes #4762.
    
    R=daniel.morsing, adg
    CC=golang-dev
    https://golang.org/cl/7308045

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

https://github.com/golang/go/commit/02e05817ada2bd3d0492387a032b1aa879c2cd3f

元コミット内容

sync: add caution about where to call (*WaitGroup).Add
    
Fixes #4762.
    
R=daniel.morsing, adg
CC=golang-dev
https://golang.org/cl/7308045

変更の背景

この変更は、Go言語の sync.WaitGroup を使用する際によく発生するバグ、特に「AddWait の後に呼ばれる」という競合状態の問題に対処するために行われました。WaitGroup は、複数のゴルーチンが完了するまで待機するために使用される同期プリミティブです。しかし、wg.Add(delta) の呼び出しが、対応するゴルーチンが起動する前、または wg.Wait() が呼び出される前に適切に行われない場合、WaitGroup が意図した数のゴルーチンを待機せずにゼロになり、プログラムが早期に終了したり、予期せぬ動作を引き起こしたりする可能性があります。

具体的には、Issue #4762 で報告された問題に対応しています。この問題は、wg.Add(1) がゴルーチン内部で呼び出される場合に発生しやすく、メインゴルーチンが wg.Wait() に到達する前に、すべてのゴルーチンが wg.Add(1) を呼び出す機会を得られない可能性があるというものです。これにより、WaitGroup のカウンターがまだゼロの状態で Wait() が実行され、待機せずに通過してしまうというバグが発生します。

このコミットは、コードの動作を変更するのではなく、WaitGroup.Add メソッドのドキュメンテーションに明確な警告を追加することで、開発者がこの一般的な落とし穴を回避できるようにすることを目的としています。

前提知識の解説

Go言語の並行処理とゴルーチン (Goroutines)

Go言語は、軽量な並行処理の仕組みとして「ゴルーチン (goroutine)」を提供します。ゴルーチンは、OSのスレッドよりもはるかに軽量で、数百万個を同時に実行することも可能です。go キーワードを使って関数呼び出しの前に置くことで、その関数を新しいゴルーチンとして実行できます。

sync パッケージと同期プリミティブ

Go言語の標準ライブラリ sync パッケージは、並行処理における同期のためのプリミティブを提供します。これには、ミューテックス (sync.Mutex)、条件変数 (sync.Cond)、そしてこのコミットで扱われる WaitGroup などがあります。

sync.WaitGroup

sync.WaitGroup は、複数のゴルーチンが完了するまでメインゴルーチンが待機するためのメカニズムです。これは、カウンターを内部に持ち、以下の3つのメソッドで操作されます。

  • Add(delta int): WaitGroup のカウンターに delta を加算します。delta は正の値でも負の値でも構いません。通常、新しいゴルーチンを起動する前に Add(1) を呼び出してカウンターを増やし、待機するゴルーチンの数を指定します。
  • Done(): WaitGroup のカウンターを1減らします。これは Add(-1) と同等です。通常、ゴルーチンの処理が完了したときに defer wg.Done() として呼び出されます。
  • Wait(): WaitGroup のカウンターがゼロになるまでブロックします。カウンターがゼロになると、Wait() はブロックを解除し、実行を続行します。

競合状態 (Race Condition)

競合状態とは、複数の並行プロセスやスレッドが共有リソースにアクセスする際に、そのアクセス順序によって結果が非決定的に変わってしまう状況を指します。WaitGroup の「AddWait の後に呼ばれる」問題は、まさにこの競合状態の一例です。Wait() がカウンターがゼロの状態で実行されてしまうと、意図した待機が行われないため、プログラムのロジックが破綻します。

技術的詳細

このコミットは、src/pkg/sync/waitgroup.go ファイルの WaitGroup.Add メソッドのコメントに新しい説明を追加しています。追加されたコメントは、Add メソッドの呼び出しタイミングに関する重要なガイドラインを提供します。

変更前のコメントは以下の通りでした。

// Add adds delta, which may be negative, to the WaitGroup counter.
// If the counter becomes zero, all goroutines blocked on Wait() are released.
// If the counter goes negative, Add panics.

このコミットによって、以下の行が追加されました。

// Note that calls with positive delta must happen before the call to Wait,
// or else Wait may wait for too small a group. Typically this means the calls
// to Add should execute before the statement creating the goroutine or
// other event to be waited for. See the WaitGroup example.

この追加されたコメントのポイントは以下の通りです。

  1. Add の呼び出しタイミング: 「正の delta を持つ呼び出しは、Wait の呼び出しよりも前に発生しなければならない」と明記しています。これは、WaitGroup のカウンターが正しく設定されることを保証するための最も重要なルールです。
  2. 潜在的な問題: もしこのルールが守られない場合、「Wait が小さすぎるグループを待つ可能性がある」と警告しています。これは、カウンターが意図した数よりも少ない状態で Wait が実行され、結果として一部のゴルーチンが完了する前に Wait が解除されてしまうことを意味します。
  3. 推奨されるプラクティス: 「通常、Add の呼び出しは、ゴルーチンを作成するステートメント、または待機する他のイベントの前に実行されるべきである」と具体的なガイダンスを提供しています。これは、go func() { ... } の前に wg.Add(1) を置くという、Goにおける WaitGroup の標準的なイディオムを強調しています。
  4. 例への参照: 「WaitGroup の例を参照してください」と、より具体的な使用例がドキュメントの他の場所にあることを示唆しています。

この変更は、Goの標準ライブラリのドキュメンテーションの品質向上に貢献し、開発者がより堅牢な並行プログラムを作成するのを助けます。コードの動作自体は変更されませんが、開発者の理解を深め、一般的なバグを未然に防ぐ効果があります。

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

変更は src/pkg/sync/waitgroup.go ファイルの WaitGroup 型の Add メソッドのコメント部分です。

--- a/src/pkg/sync/waitgroup.go
+++ b/src/pkg/sync/waitgroup.go
@@ -34,8 +34,13 @@ type WaitGroup struct {
 // G3: Wait() // G1 still hasn\'t run, G3 finds sema == 1, unblocked! Bug.\n \n // Add adds delta, which may be negative, to the WaitGroup counter.\n-// If the counter becomes zero, all goroutines blocked on Wait() are released.\n+// If the counter becomes zero, all goroutines blocked on Wait are released.\n // If the counter goes negative, Add panics.\n+//\n+// Note that calls with positive delta must happen before the call to Wait,\n+// or else Wait may wait for too small a group. Typically this means the calls\n+// to Add should execute before the statement creating the goroutine or\n+// other event to be waited for. See the WaitGroup example.\n func (wg *WaitGroup) Add(delta int) {\n \tif raceenabled {\n \t\traceReleaseMerge(unsafe.Pointer(wg))\n```

## コアとなるコードの解説

このコミットは、`sync.WaitGroup` の `Add` メソッドのドキュメンテーションに、`Add` の呼び出しタイミングに関する重要な注意点を追加しています。

追加されたコメントは以下の通りです。

```go
// Note that calls with positive delta must happen before the call to Wait,
// or else Wait may wait for too small a group. Typically this means the calls
// to Add should execute before the statement creating the goroutine or
// other event to be waited for. See the WaitGroup example.

このコメントは、WaitGroup を正しく使用するための最も重要な原則の一つを強調しています。それは、wg.Add(n) (nは正の数) の呼び出しは、wg.Wait() の呼び出しよりも常に前に行われるべきである、という点です。

なぜこれが重要なのでしょうか?

もし wg.Add(1) がゴルーチン内部で呼び出され、かつそのゴルーチンの起動が遅れた場合、メインゴルーチンが wg.Wait() に到達した時点で、WaitGroup のカウンターがまだゼロである可能性があります。この場合、Wait() はブロックせずにすぐに戻ってしまい、まだ開始されていない、または Add(1) を呼び出していないゴルーチンを待つことができません。結果として、プログラムは意図した処理が完了する前に終了してしまう可能性があります。

このコメントは、この一般的な落とし穴を明示的に警告し、開発者に対して、ゴルーチンを起動するAdd を呼び出すという推奨されるプラクティスに従うよう促しています。これにより、WaitGroup のカウンターが、待機すべきゴルーチンの数を正確に反映していることが保証され、競合状態によるバグを防ぐことができます。

関連リンク

  • Go言語 sync パッケージのドキュメンテーション: https://pkg.go.dev/sync
  • Go言語 sync.WaitGroup のドキュメンテーション: https://pkg.go.dev/sync#WaitGroup
  • Go言語の並行処理に関する公式ブログ記事やドキュメント (一般的な情報源)

参考にした情報源リンク