Go言語で並行処理を実装する際に、sync.WaitGroup
は複数のゴルーチン(軽量スレッド)を管理するために役立つツールです。並行処理を適切に管理することで、パフォーマンスを大幅に向上させることが可能ですが、その一方で、各ゴルーチンの終了を待つ必要が生じる場合があります。このような場面でsync.WaitGroup
を使うことで、プログラム全体の同期をとりながら、効率よく並行処理を行うことができます。
本記事では、sync.WaitGroup
の基本的な使い方から、ループと組み合わせた並行処理の実装例までを詳細に解説し、Go言語における効果的な並行処理の方法を学びます。
sync.WaitGroupの基本概要
Go言語におけるsync.WaitGroup
は、並行処理の完了を待つために使用される同期プリミティブです。これにより、複数のゴルーチンが同時に実行される場面で、全てのゴルーチンの処理が完了するまで待機することができます。
sync.WaitGroup
は、特定の数のゴルーチンをカウントし、各ゴルーチンの処理が終了するたびにそのカウントを減らします。カウントがゼロになるまで待機することで、全てのゴルーチンが終了したことを確認する仕組みです。この仕組みを利用することで、メインのプログラムが並行処理の完了を確実に待つことが可能となり、処理の順序や完了タイミングを適切に制御できます。
次章では、sync.WaitGroup
の各メソッドについて詳しく説明します。
Add・Done・Waitメソッドの役割
sync.WaitGroup
を効果的に使うためには、主要なメソッドであるAdd
、Done
、Wait
の役割を理解することが重要です。それぞれのメソッドの役割と使い方を見ていきましょう。
Addメソッド
Add
メソッドは、待機するゴルーチンの数を増加させます。この数は「カウント」としてsync.WaitGroup
に保持され、ここで指定したカウント数だけDone
メソッドが呼ばれるまで、Wait
メソッドがブロックされます。たとえば、3つのゴルーチンを待機する場合、wg.Add(3)
と指定します。
Doneメソッド
Done
メソッドは、カウントを1減少させる役割を果たします。各ゴルーチンの終了時にDone
を呼び出すことで、sync.WaitGroup
に登録されたゴルーチンの終了が通知され、全てのゴルーチンが完了するまでのカウントが徐々に減っていきます。このメソッドを呼び忘れると、カウントが減らずWait
が解除されないため、注意が必要です。
Waitメソッド
Wait
メソッドは、sync.WaitGroup
のカウントがゼロになるまでブロックされます。これにより、全てのゴルーチンが終了するまで処理を待機することができます。メイン関数や他の重要な処理が、全てのゴルーチンの終了を待ちたい場合に使用されます。
これらのメソッドを組み合わせることで、sync.WaitGroup
は並行処理を安全かつ確実に制御し、全ての処理が完了するまで待機する仕組みを提供します。
並行処理とsync.WaitGroupの基本実装例
sync.WaitGroup
を使った並行処理の基本的な実装方法を具体的なコード例で解説します。この例では、複数のゴルーチンを起動し、全てのゴルーチンが完了するまで待機する仕組みを紹介します。
コード例:sync.WaitGroupを使ったゴルーチンの管理
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // ゴルーチンの完了を通知
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second) // 擬似的な処理時間
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
numWorkers := 3
// ゴルーチンの数を設定
wg.Add(numWorkers)
for i := 1; i <= numWorkers; i++ {
go worker(i, &wg) // ゴルーチンの起動
}
wg.Wait() // 全てのゴルーチンが完了するまで待機
fmt.Println("All workers done.")
}
コード解説
- worker関数:
worker
関数は、擬似的な処理を行うゴルーチンとして定義されています。関数内でdefer wg.Done()
を使ってゴルーチンの完了を通知します。 - Addメソッドの設定:メイン関数内で、
wg.Add(numWorkers)
を使って、待機するゴルーチンの数を設定しています。 - Waitメソッドの待機:メイン関数の最後で
wg.Wait()
を呼び出し、全てのworker
ゴルーチンが終了するまで待機しています。
このコード例では、3つのゴルーチンが並行して処理され、全ての処理が完了した後に「All workers done.」というメッセージが表示されます。これにより、sync.WaitGroup
を使って安全に並行処理を管理する方法がわかります。
ループとsync.WaitGroupの組み合わせ
sync.WaitGroup
をループと組み合わせて使うことで、数多くの並行タスクを簡潔に起動し、それらの完了を効率的に待機することができます。ここでは、ループ内でsync.WaitGroup
を使って並行処理を管理する方法と、その際の注意点を解説します。
ループでのsync.WaitGroupの使用方法
例えば、データのリストを並行処理する場合や、同じ関数を複数のゴルーチンで処理したい場合、ループ内でゴルーチンを起動することが一般的です。このとき、sync.WaitGroup
を使って各ゴルーチンの終了を待つように設定します。
コード例:ループとsync.WaitGroupを用いた並行処理
package main
import (
"fmt"
"sync"
"time"
)
func processTask(taskId int, wg *sync.WaitGroup) {
defer wg.Done() // ゴルーチンの完了を通知
fmt.Printf("Task %d processing\n", taskId)
time.Sleep(time.Second) // 処理の擬似的な待機時間
fmt.Printf("Task %d completed\n", taskId)
}
func main() {
var wg sync.WaitGroup
numTasks := 5
// 各タスクを並行に処理するゴルーチンを起動
for i := 1; i <= numTasks; i++ {
wg.Add(1) // ゴルーチンの数を追加
go processTask(i, &wg) // 各タスクのゴルーチンを起動
}
wg.Wait() // 全てのゴルーチンが完了するまで待機
fmt.Println("All tasks completed.")
}
コード解説
- Addメソッドの位置:ループ内で
wg.Add(1)
を呼び出し、各タスクごとにカウントを増やしています。こうすることで、ゴルーチンごとに個別に完了を追跡できます。 - defer wg.Done():各ゴルーチンが完了すると
wg.Done()
を呼び出し、待機カウントを減少させます。 - wg.Wait():メイン関数は
wg.Wait()
で全ゴルーチンが完了するまでブロックされ、すべてのタスクが完了した時点で「All tasks completed.」が出力されます。
注意点
- wg.Addの呼び出し場所:
go
ステートメントの外側でwg.Add()
を実行しないと、タイミングによってはDone
が呼ばれる前にWait
が終了してしまうリスクがあります。 - 共有変数の扱い:ループ内で並行処理を行う際、共有変数がある場合は注意が必要です。必要に応じてミューテックスを使い、データ競合を防ぐようにしましょう。
このように、ループとsync.WaitGroup
を組み合わせることで、大量のタスクを並行して効率的に処理することができます。
ゴルーチンとsync.WaitGroupの連携
Go言語において、sync.WaitGroup
とゴルーチンの連携は並行処理を安全かつ効率的に管理するために重要です。この章では、sync.WaitGroup
とゴルーチンを組み合わせて並行処理を行う際の基本的なパターンや、ベストプラクティスを解説します。
sync.WaitGroupとゴルーチンの連携の基本パターン
sync.WaitGroup
を使ってゴルーチンを管理する際、基本的には以下のような流れになります:
- Addメソッドでカウントを設定:メイン関数内で実行するゴルーチンの数を
wg.Add()
で設定します。 - goステートメントでゴルーチンを起動:各ゴルーチンは、関数内で
defer wg.Done()
を呼び出して終了時にカウントを減らすようにします。 - Waitメソッドで終了を待つ:メイン関数や別の関数内で
wg.Wait()
を呼び出し、全てのゴルーチンの終了を待機します。
コード例:sync.WaitGroupとゴルーチンの基本連携
package main
import (
"fmt"
"sync"
"time"
)
func fetchData(id int, wg *sync.WaitGroup) {
defer wg.Done() // 終了時にカウントを減らす
fmt.Printf("Fetching data for ID: %d\n", id)
time.Sleep(time.Millisecond * 500) // 擬似的なデータ取得時間
fmt.Printf("Data fetched for ID: %d\n", id)
}
func main() {
var wg sync.WaitGroup
ids := []int{1, 2, 3, 4, 5}
for _, id := range ids {
wg.Add(1) // カウントを1増やす
go fetchData(id, &wg) // 各データ取得のゴルーチンを起動
}
wg.Wait() // 全てのゴルーチンの完了を待つ
fmt.Println("All data fetching completed.")
}
コード解説
- fetchData関数:各データを取得する処理を模倣した関数で、
defer wg.Done()
によって終了時にカウントが1減少します。 - メイン関数のループ処理:
ids
スライスに含まれるIDごとにfetchData
を並行処理として起動します。各ゴルーチンの開始前にwg.Add(1)
でカウントを増やして管理しています。 - wg.Wait():メイン関数の最後で
wg.Wait()
を呼び出すことで、全てのfetchData
ゴルーチンが終了するまで待機します。
ベストプラクティス
- defer wg.Done()の使用:各ゴルーチン内で必ず
defer wg.Done()
を呼び出すことで、終了時にカウントが減るようにします。このパターンにより、各ゴルーチンが終了時に確実にカウントを減らし、リソースが適切に解放されることが保証されます。 - 同期ポイントの設置:メイン処理の中で
wg.Wait()
を用いることで、必要な全ての並行処理が完了するまでメイン処理が待機するようになります。これにより、プログラムの一貫性が保たれます。
このように、sync.WaitGroup
とゴルーチンを連携させることで、Go言語における並行処理がスムーズに管理できるようになります。
並行処理時のエラーハンドリング
Go言語でsync.WaitGroup
とゴルーチンを使って並行処理を行う際、エラーハンドリングの実装は非常に重要です。複数のゴルーチンが同時に実行される環境では、エラーが発生した場合にそれを適切に処理しないと、期待通りの結果が得られないだけでなく、プログラム全体の信頼性にも影響を与えます。
この章では、並行処理時のエラーハンドリング方法について、sync.WaitGroup
とゴルーチンの組み合わせにおける具体的な実装方法を解説します。
基本的なエラーハンドリング戦略
並行処理におけるエラーハンドリングには、いくつかの一般的な戦略があります:
- チャンネルを使ってエラーを収集する:エラーをチャンネルに送信し、メインスレッドで処理します。
- 同期オブジェクトでエラー状態を管理する:
sync.Mutex
などを使ってエラーの状態をスレッドセーフに管理します。 - contextパッケージを使ったキャンセル処理:エラーが発生した場合に他のゴルーチンをキャンセルするために、
context
を利用することも一般的です。
コード例:チャンネルを使ったエラーハンドリング
以下に、エラーチャンネルを利用したsync.WaitGroup
のエラーハンドリング例を示します。
package main
import (
"errors"
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup, errChan chan<- error) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Millisecond * 500)
// エラーチェックのための条件を設定
if id == 3 {
errChan <- errors.New("error in worker 3")
return
}
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
errChan := make(chan error, 1) // バッファ付きのエラーチャンネル
numWorkers := 5
for i := 1; i <= numWorkers; i++ {
wg.Add(1)
go worker(i, &wg, errChan)
}
// ゴルーチンの終了を待つ
go func() {
wg.Wait()
close(errChan) // エラーチャンネルを閉じる
}()
// エラーのチェック
for err := range errChan {
if err != nil {
fmt.Println("Received error:", err)
break
}
}
fmt.Println("All workers done.")
}
コード解説
- エラーチャンネル:
errChan
はエラーを格納するためのチャンネルで、エラーが発生したゴルーチンからエラーメッセージを送信します。 - エラー発生時の動作:
worker
関数内で特定の条件が満たされた場合、エラーメッセージをチャンネルに送信し、処理を終了します。 - エラーの受け取り:メイン関数内で
range
を使ってerrChan
を監視し、エラーがあれば表示します。
ベストプラクティス
- チャンネルを使ったエラーハンドリング:並行処理時のエラーをチャンネルで処理することで、メインスレッドでエラーを一元管理できます。
- エラーチャンネルのクローズ:
Wait
が完了した後にチャンネルを閉じることで、エラーがすべて収集された状態でクリーンに終了できます。 - エラーハンドリングの一貫性:チャンネルや
sync.Mutex
を活用し、並行処理内のエラーハンドリングが一貫していることが重要です。
このように、チャンネルや他の同期オブジェクトを活用してエラーを適切に管理することで、Goの並行処理を安全かつ効率的に進めることができます。
応用例:大量データ処理におけるsync.WaitGroupの活用
sync.WaitGroup
は、Go言語で大量データの並行処理を行う際に非常に有効です。特に、データの並列処理やバッチ処理が必要な場合、sync.WaitGroup
を用いることで効率よく処理を管理し、リソースを最大限に活用することが可能です。ここでは、大量データを並行処理する実践的な応用例とその実装について解説します。
シナリオ:大量のファイル処理
例えば、数百のファイルを読み込み、各ファイルに対して特定の処理を行う場合を考えてみます。シーケンシャルに処理を行うと時間がかかりすぎるため、ファイルごとにゴルーチンを使って並行に処理し、全ファイルが処理完了するまで待つように実装します。
コード例:sync.WaitGroupによる並行ファイル処理
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
func processFile(fileId int, wg *sync.WaitGroup) {
defer wg.Done() // 終了時にカウントを減らす
fmt.Printf("Processing file %d\n", fileId)
time.Sleep(time.Millisecond * time.Duration(rand.Intn(500))) // 擬似的な処理時間
fmt.Printf("File %d processed\n", fileId)
}
func main() {
var wg sync.WaitGroup
numFiles := 20 // 処理するファイルの数
for i := 1; i <= numFiles; i++ {
wg.Add(1)
go processFile(i, &wg) // ファイル処理を並行に実行
}
wg.Wait() // 全ファイル処理の完了を待機
fmt.Println("All files processed.")
}
コード解説
- processFile関数:各ファイルの処理を担当する関数で、擬似的な処理時間を設け、終了時に
wg.Done()
を呼び出してカウントを減少させます。 - メイン関数のループ:
numFiles
で定義されたファイル数だけループを回し、各ファイルを並行処理として起動します。各ゴルーチン起動前にwg.Add(1)
を呼び出し、カウントを増加させています。 - wg.Wait()の使用:全てのファイル処理が完了するまで
wg.Wait()
で待機し、処理が完了すると「All files processed.」が出力されます。
実用的なテクニック
- バッチ処理の導入:大量データを扱う場合、一度に起動するゴルーチン数を制限することでシステム負荷を抑え、安定的な処理が可能です。例えば、セマフォを用いて一度に起動するゴルーチン数を制限し、適宜追加することで、システムリソースの節約が可能です。
- エラーハンドリングの追加:大量データ処理では一部のデータ処理でエラーが発生する可能性があります。エラーチャンネルやミューテックスを活用し、エラー発生時のデータを収集してログ出力するなど、エラーハンドリングの強化が必要です。
- 並列度の制御:
sync.WaitGroup
を使った大量データ処理では、ハードウェアの性能やネットワーク負荷に応じて並列度を調整することが重要です。環境に応じてゴルーチンの数を調整し、過負荷にならないようにします。
このように、sync.WaitGroup
を利用することで大量データ処理を効率的に管理することができます。ファイル処理以外にも、APIリクエストやデータベースクエリなど、さまざまな並行処理に応用することが可能です。
演習問題:sync.WaitGroupの使い方を学ぶ
ここでは、sync.WaitGroup
を使った並行処理の理解を深めるための演習問題を紹介します。この問題を解くことで、Go言語における並行処理とsync.WaitGroup
の活用方法について実践的な理解を深めることができます。
問題:並行ウェブリクエストの処理
以下の要件に基づいて、Goプログラムを作成してください。
- 複数のウェブサイトに並行してリクエストを送り、それぞれの応答時間を計測します。
- サイトのURLは配列として与えられます。
- 各リクエストはゴルーチンで並行に処理し、すべてのリクエストが完了するまで待機します。
sync.WaitGroup
を使用して、すべてのゴルーチンの完了を待機するように実装してください。- 各サイトの応答時間をターミナルに出力し、すべての処理が完了した後に「All requests completed.」と表示されるようにします。
ヒント
- http.Get を使用してHTTPリクエストを送信し、レスポンスを取得します。
- 各ゴルーチン内で
defer wg.Done()
を使用して、処理の終了を通知することを忘れないようにしてください。 - 応答時間の計測には、
time.Now()
やtime.Since()
を利用すると便利です。
解答例
以下に解答例を示します。これを参考にしつつ、自分でも挑戦してみましょう。
package main
import (
"fmt"
"net/http"
"sync"
"time"
)
func fetchURL(url string, wg *sync.WaitGroup) {
defer wg.Done() // ゴルーチン終了時にカウントを減少
start := time.Now()
resp, err := http.Get(url)
if err != nil {
fmt.Printf("Error fetching %s: %v\n", url, err)
return
}
defer resp.Body.Close()
duration := time.Since(start)
fmt.Printf("Fetched %s in %v\n", url, duration)
}
func main() {
var wg sync.WaitGroup
urls := []string{
"https://www.google.com",
"https://www.github.com",
"https://www.stackoverflow.com",
}
for _, url := range urls {
wg.Add(1)
go fetchURL(url, &wg) // 各URLのゴルーチンを起動
}
wg.Wait() // 全てのゴルーチンの完了を待機
fmt.Println("All requests completed.")
}
実装後の確認
- 各URLへの応答時間がターミナルに表示されることを確認してください。
- すべてのリクエストが完了した後に「All requests completed.」が表示されれば成功です。
演習問題のポイント
この演習を通じて、sync.WaitGroup
を活用して複数の並行処理を管理する方法や、エラーハンドリング、レスポンスタイムの計測を学ぶことができます。
まとめ
本記事では、Go言語における並行処理のためのsync.WaitGroup
の基本的な使い方と、その応用方法について解説しました。sync.WaitGroup
は、複数のゴルーチンを安全に管理し、全ての処理が完了するまで待機するための強力なツールです。基本メソッドの役割や、ループとの組み合わせ、エラーハンドリング、そして大量データ処理への応用方法を学ぶことで、並行処理を効率的に管理するスキルを身につけることができたでしょう。今後、Go言語でのプロジェクトにおいて、sync.WaitGroup
を活用して、スムーズで安定した並行処理を実現してください。
コメント