sync.WaitGroup
CoddyのGOジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 64/107。
Goroutines Basics(ゴルーチンの基本)のレッスンでは、ゴルーチンの終了を待機するために time.Sleep を使用しましたが、これは不安定な手法です。sync.WaitGroup は、複数のゴルーチンが処理を完了するのを待機するための適切な方法を提供します。
WaitGroupはカウンターのように動作します。ゴルーチンを開始する前にインクリメントし、ゴルーチンが終了したときにデクリメントし、カウンターがゼロになるまでブロックします:
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // カウンタをインクリメントする
go func(id int) {
defer wg.Done() // 終了時にデクリメントする
fmt.Printf("Worker %d finished\n", id)
}(i)
}
wg.Wait() // カウンタが0になるまで待機する
fmt.Println("All workers complete")
}3つの主要なメソッドは、カウンターをnだけ増やすためのAdd(n)、カウンターを1つ減らすためのDone()(Add(-1)に相当します)、そしてカウンターがゼロになるまでブロックするためのWait()です。defer wg.Done()を使用することで、ゴルーチンがパニックを起こした場合でもカウンターが確実にデクリメントされます。
よくある間違いは、ゴルーチンの前ではなく、ゴルーチンの中で Add を呼び出すことです。これにより、すべてのゴルーチンが登録される前に Wait が戻ってしまう可能性があるというレースコンディション(競合状態)が発生します。
// 誤り - 競合状態
go func() {
wg.Add(1) // Wait() の後に実行される可能性があります
defer wg.Done()
}()
// 正解 - 起動前に Add する
wg.Add(1)
go func() {
defer wg.Done()
}()WaitGroupsは、並行操作を調整するために構造体に埋め込まれることが多く、Goでスレッドセーフな型を構築するために不可欠です。
チャレンジ
簡単sync.WaitGroupを使用して、複数の並行ファイルダウンロードを調整するダウンロードマネージャーを構築しましょう。このマネージャーは、任意のスリープタイマーに頼ることなく、すべてのダウンロードがいつ完了するかを追跡します。
コードは2つのファイルに分けて構成します:
downloader.go: ダウンロードの調整ロジックを定義します。ID(int)、Filename(string)、Size(int、KB単位) フィールドを持つDownload構造体を作成します。DownloadにProcess(wg *sync.WaitGroup)メソッドを実装します。これは、ファイルサイズに基づいた時間(Size * 10ミリ秒を使用)だけスリープすることでダウンロードをシミュレートします。ダウンロードが完了したら、Downloaded: [Filename] ([Size]KB)と出力します。WaitGroupのカウンターが適切にデクリメントされるように、defer wg.Done()を使用してください。ゴルーチンを使用してすべてのダウンロードを並行して開始し、それらがすべて完了するのを待機する
StartDownloads(downloads []Download) int関数を作成します。各ゴルーチンを起動する前に、ゴルーチン内ではなく、起動前にwg.Add(1)を呼び出すことを忘れないでください。すべてのダウンロードの合計サイズを返します。main.go: ダウンロード情報を読み取り、並行ダウンロードをオーケストレートします。ダウンロード数を読み取り、次に各ダウンロードについてID、ファイル名、サイズを読み取ります。ダウンロードを作成し、それらを
StartDownloadsに渡します。すべてのダウンロードが完了した後、All downloads complete: [total]KB totalと出力します。
以下の入力が提供されます:
- 1行目:ダウンロード数(整数)
- 続く行:各ダウンロードについて、ID(整数)、ファイル名(文字列)、KB単位のサイズ(整数)の3行
例えば、以下のように与えられた場合:
3
1
report.pdf
20
2
image.png
5
3
data.csv
10出力には、ダウンロードの完了(スリープ時間が短いため、小さいファイルが先に終了します)が表示され、その後にサマリーが表示されるはずです:
Downloaded: image.png (5KB)
Downloaded: data.csv (10KB)
Downloaded: report.pdf (20KB)
All downloads complete: 35KB totaltime.Sleep を使用する場合との主な違いは、wg.Wait() がすべてのゴルーチンが Done() で完了を通知するまでブロックすることです。これにより、各ダウンロードにどれくらいの時間がかかるかに関係なく、正確な同期が可能になります。
チートシート
sync.WaitGroup は、ゴルーチンの完了を待機するための適切な方法を提供します。これは、実行中のゴルーチンを追跡するカウンターのように機能します。
3つの主要なメソッドは以下の通りです:
Add(n)- カウンターを n だけ増やしますDone()- カウンターを 1 つ減らします(Add(-1)と同等です)Wait()- カウンターがゼロになるまでブロックします
基本的な使用パターン:
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // increment before starting goroutine
go func(id int) {
defer wg.Done() // decrement when done
fmt.Printf("Worker %d finished\n", id)
}(i)
}
wg.Wait() // block until all goroutines complete
fmt.Println("All workers complete")defer wg.Done() を使用することで、ゴルーチンがパニックを起こした場合でも確実にカウンターを減らすことができます。
重要: レースコンディションを避けるため、Add はゴルーチン内ではなく、必ずゴルーチンを起動する前に呼び出してください:
// Wrong - race condition
go func() {
wg.Add(1) // might run after Wait()
defer wg.Done()
}()
// Correct
wg.Add(1)
go func() {
defer wg.Done()
}()自分で試してみよう
package main
import (
"fmt"
)
func main() {
// ダウンロード数を読み込む
var n int
fmt.Scanln(&n)
// ダウンロード情報を読み込む
downloads := make([]Download, n)
for i := 0; i < n; i++ {
var id int
var filename string
var size int
fmt.Scanln(&id)
fmt.Scanln(&filename)
fmt.Scanln(&size)
downloads[i] = Download{
ID: id,
Filename: filename,
Size: size,
}
}
// TODO: downloadsスライスを使用してStartDownloadsを呼び出す
// TODO: サマリーを表示する: "All downloads complete: [total]KB total"
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
1Go オブジェクト指向の基礎
外部ファイルGo ワークスペースとモジュールパッケージとインポート公開された名前と非公開の名前Go におけるオブジェクト指向入門クラスとしての構造体構造体へのメソッド定義ポインタレシーバと値レシーバ構造体の初期化コンストラクタ関数まとめ:簡易計算機8エラー処理とOOP
error インターフェースカスタムエラー型エラーラッピング (fmt.Errorf)センチネルエラーerrors.Is() と errors.As()Panic、Defer、Recover復習 - ファイルパーサー