Menu
Coddy logo textTech

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でスレッドセーフな型を構築するために不可欠です。

challenge icon

チャレンジ

簡単

sync.WaitGroupを使用して、複数の並行ファイルダウンロードを調整するダウンロードマネージャーを構築しましょう。このマネージャーは、任意のスリープタイマーに頼ることなく、すべてのダウンロードがいつ完了するかを追跡します。

コードは2つのファイルに分けて構成します:

  • downloader.go: ダウンロードの調整ロジックを定義します。

    ID (int)、Filename (string)、Size (int、KB単位) フィールドを持つ Download 構造体を作成します。

    DownloadProcess(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 total

time.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"
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン