Goにおける複数のゴルーチンのための待ち方:4つのエッセンシャルなメソッド
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Goでは、メインゴルーチンが、他のゴルーチンがタスクを完了するのを待ってから、実行を継続したり、プログラムを終了したりする必要があることがよくあります。これは、並行同期の一般的な要件です。Goには、シナリオと要件に応じて、これを実現するためのいくつかのメカニズムが用意されています。
メソッド1:sync.WaitGroupの使用
sync.WaitGroup
は、Goで最も一般的に使用される同期ツールであり、ゴルーチンのグループがタスクを完了するのを待つように設計されています。これは、カウンターのメカニズムを通じて機能し、メインゴルーチンが複数のサブゴルーチンを待つ必要がある場合に特に適しています。
コード例
package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup // 3つのゴルーチンを開始します for i := 1; i <= 3; i++ { wg.Add(1) // カウンターを1ずつ増やします go func(id int) { defer wg.Done() // タスクが完了したら、カウンターを1ずつ減らします fmt.Printf("ゴルーチン %d が実行中です\n", id) }(i) } wg.Wait() // メインゴルーチンは、すべてのゴルーチンが完了するのを待ちます fmt.Println("すべてのゴルーチンが完了しました") }
出力(順序は異なる場合があります):
ゴルーチン 1 が実行中です ゴルーチン 2 が実行中です ゴルーチン 3 が実行中です すべてのゴルーチンが完了しました
仕組み:
wg.Add(n)
: 待機するゴルーチンの数を示すために、カウンターを増やします。wg.Done()
: 完了時に各ゴルーチンによって呼び出され、カウンターを1ずつ減らします。wg.Wait()
: カウンターがゼロになるまで、メインゴルーチンをブロックします。
利点:
- シンプルで使いやすく、固定数のゴルーチンに適しています。
- 追加のチャネルは不要で、パフォーマンスのオーバーヘッドが低くなっています。
メソッド2:チャネルの使用
チャネルを通じてシグナルを渡すことにより、メインゴルーチンは、他のすべてのゴルーチンが完了シグナルを送信するまで待つことができます。この方法はより柔軟性がありますが、通常はWaitGroupよりも少し複雑です。
コード例
package main import "fmt" func main() { // 完了を通知するシグナルチャネルです 完了 := make(chan struct{}) numGoroutines := 3 for i := 1; i <= numGoroutines; i++ { go func(id int) { fmt.Printf("ゴルーチン %d が実行中です\n", id) 完了 <- struct{}{} }(i) } // すべてのゴルーチンが完了するのを待ちます for i := 0; i < numGoroutines; i++ { <-完了 // シグナルを受信します } fmt.Println("すべてのゴルーチンが完了しました") }
出力(順序は異なる場合があります):
ゴルーチン 1 が実行中です ゴルーチン 2 が実行中です ゴルーチン 3 が実行中です すべてのゴルーチンが完了しました
仕組み:
- 各ゴルーチンは、完了時に
done
チャネルにシグナルを送信します。 - メインゴルーチンは、指定された数のシグナルを受信することにより、すべてのタスクが完了したことを確認します。
利点:
- 柔軟性が高く、データ(タスクの結果など)を運ぶことができます。
- 動的な数のゴルーチンに適しています。
短所:
- 受信数を手動で管理する必要があるため、コードが少し煩雑になる可能性があります。
メソッド3:コンテキストを使用した終了の制御
context.Context
を使用すると、ゴルーチンの終了を正常に制御し、メインゴルーチンがすべてのタスクが完了するまで待機できます。この方法は、キャンセルまたはタイムアウトが必要なシナリオで特に役立ちます。
コード例
package main import ( "context" "fmt" "sync" ) func main() { ctx, cancel := context.WithCancel(context.Background()) var wg sync.WaitGroup for i := 1; i <= 3; i++ { wg.Add(1) go func(id int) { defer wg.Done() select { case <-ctx.Done(): fmt.Printf("ゴルーチン %d はキャンセルされました\n", id) return default: fmt.Printf("ゴルーチン %d が実行中です\n", id) } }(i) } // タスクの完了をシミュレートします cancel() // キャンセルシグナルを送信します // すべてのゴルーチンが終了するのを待ちます wg.Wait() fmt.Println("すべてのゴルーチンが完了しました") }
出力(キャンセルが発生するタイミングによって異なる場合があります):
ゴルーチン 1 が実行中です ゴルーチン 2 が実行中です ゴルーチン 3 が実行中です すべてのゴルーチンが完了しました
仕組み:
- コンテキストは、ゴルーチンに終了を通知するために使用されます。
- WaitGroupは、メインゴルーチンがすべてのゴルーチンが完了するのを待つようにします。
利点:
- キャンセルとタイムアウトをサポートし、複雑な同時実行シナリオに適しています。
短所:
- コードが少し複雑になります。
メソッド4:errgroupの使用(推奨)
golang.org/x/sync/errgroup
は、WaitGroupの待機機能とエラー処理を組み合わせた高度なツールであり、タスクのグループを待機し、エラーを処理するのに特に適しています。
コード例
package main import ( "fmt" "golang.org/x/sync/errgroup" ) func main() { var g errgroup.Group for i := 1; i <= 3; i++ { // ループ変数を使用しないために、ローカル変数にコピーします // See: https://go.dev/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables // and: https://stackoverflow.com/questions/56337579/go-range-loop-variable // and: https://go.dev/tour/concurrency/5 id := i g.Go(func() error { fmt.Printf("ゴルーチン %d が実行中です\n", id) return nil //エラーはありません }) } /* // Alternative implementation that avoids loop issues. var g errgroup.Group var i int for i = 1; i <= 3; i++ { g.Go(func(i int) func() error { return func() error { fmt.Printf("Goroutine %d is running\n", i) return nil // No error } }(i)) } */ // g.Wait() can safely return an error // so always handle that. // Also, g.Wait() may silently swallow errors. Be aware! // See: https://github.com/golang/go/issues/24834 // We can also use this err value. // In any case, we must handle any error, even if we ignore it // It is possible for g.Wait() to silently swallow errors. Be aware! // See: https://github.com/golang/go/issues/24834 if err := g.Wait(); err != nil { fmt.Println("エラー:", err) } else { fmt.Println("すべてのゴルーチンが完了しました") } }
出力:
ゴルーチン 1 が実行中です ゴルーチン 2 が実行中です ゴルーチン 3 が実行中です すべてのゴルーチンが完了しました
仕組み:
g.Go()
はゴルーチンを開始し、グループに追加します。g.Wait()
は、すべてのゴルーチンが完了するのを待ってから、最初のエラー以外のエラーを返します(エラーがある場合)。
利点:
- シンプルでエレガント、エラー伝播をサポートします。
- 組み込みのコンテキストサポート(
errgroup.WithContext
を使用できます)。
インストール:
go get golang.org/x/sync/errgroup
が必要です。
どの方法を選択するか?
sync.WaitGroup
- **該当するシナリオ:**固定数のシンプルなタスク。
- **利点:**シンプルで効率的。
- **短所:**エラー処理またはキャンセルをサポートしていません。
チャネル
- **該当するシナリオ:**動的なタスクまたは結果を渡す必要がある場合。
- **利点:**柔軟性が高い。
- **短所:**手動管理がより複雑になります。
コンテキスト
- **該当するシナリオ:**キャンセルまたはタイムアウトが必要な複雑な状況。
- **利点:**キャンセルとタイムアウトをサポートします。
- **短所:**コードがやや複雑になります。
errgroup
- **該当するシナリオ:**エラー処理と待機を必要とする最新のアプリケーション。
- **利点:**エレガントで強力。
- **短所:**追加の依存関係が必要です。
その他:メインゴルーチンがスリープしないのはなぜですか?
time.Sleep
は固定の遅延を導入するだけで、タスクの完了を正確に待つことはできません。これにより、プログラムが途中で終了したり、不必要な待機が発生したりする可能性があります。同期ツールの方が信頼性があります。
概要
メインゴルーチンが他のゴルーチンを待機するための最も一般的な方法は、シンプルで効率的なsync.WaitGroup
です。エラー処理またはキャンセル機能が必要な場合は、errgroup
またはcontext
との組み合わせをお勧めします。特定の要件に応じて適切なツールを選択して、明確なプログラムロジックを確保し、リソースリークを防ぎます。
Leapcellへようこそ。Goプロジェクトのホスティングに最適です。
Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです。
多言語サポート
- Node.js、Python、Go、またはRustで開発します。
無制限のプロジェクトを無料でデプロイ
- 使用量に対してのみ支払い — リクエストも課金もありません。
比類のないコスト効率
- アイドルチャージなしの従量課金制。
- 例:25ドルで、平均応答時間60ミリ秒で694万件のリクエストをサポートします。
合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的なUI。
- 完全に自動化されたCI/CDパイプラインとGitOps統合。
- 実用的な洞察のためのリアルタイムメトリックとロギング。
簡単なスケーラビリティとハイパフォーマンス
- 高い同時実行性を簡単に処理するための自動スケーリング。
- 運用オーバーヘッドゼロ — 構築に集中するだけです。
ドキュメントで詳細をご覧ください。
Xでフォローしてください: @LeapcellHQ