Go言語でsync.WaitGroupをマスターする5つの方法

Go言語でsync.WaitGroupを学ぶ初心者の手引きのイメージGo言語
この記事は約16分で読めます。

 

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

Go言語でプログラミングを行う際、複数のタスクを効率的に管理するためにsync.WaitGroupが非常に重要です。

この記事を読むことで、Go言語の基本からsync.WaitGroupの使い方までを学び、あなたのプログラミングスキルを向上させることができます。

初心者でも理解しやすいように、基本的な概念から詳細な解説までを行います。

●Go言語とsync.WaitGroupの概要

Go言語は、Googleによって開発されたプログラミング言語で、並行処理や高速な実行速度が特徴です。

Go言語のコードはシンプルで読みやすく、大規模なシステム開発にも適しています。

また、Go言語はゴルーチンと呼ばれる軽量スレッドを用いた並行処理をサポートしており、これにより複数の処理を同時に実行することが可能になります。

○Go言語の基本

Go言語を学ぶ上で知っておくべき基本的な特徴には、静的型付け、コンパイル言語、ガベージコレクション、並行処理のサポートなどがあります。

Go言語では、コードの構造が明確で、パッケージシステムによってモジュール性が高いのも特徴です。

これにより、保守や拡張がしやすくなっています。

○sync.WaitGroupとは何か?

sync.WaitGroupは、Go言語の標準ライブラリの一部で、ゴルーチン間の同期を取るために使用されます。

これは、複数のゴルーチンがすべて終了するまで待機するための仕組みを提供します。

具体的には、sync.WaitGroupは内部でカウンタを持ち、このカウンタが0になるまでWaitメソッドはブロックされます。

ゴルーチンが開始される際にはAddメソッドでカウンタを増やし、ゴルーチンの処理が終了するとDoneメソッドを呼び出してカウンタを減らします。

この機能により、複数のゴルーチンが全て終了したことを確認してから次の処理を行うことができます。

●sync.WaitGroupの基本的な使い方

sync.WaitGroupを使用することで、Go言語における複数のゴルーチン(Goの並行処理を行うための軽量スレッド)の完了を効果的に待機することができます。

この機能は、プログラムがある特定のポイントに到達するまで、複数のゴルーチンが全て完了するのを待つために使われます。

sync.WaitGroupは、内部的にカウンタを持っており、このカウンタを使ってゴルーチンの完了を追跡します。

カウンタの初期値はゼロで、ゴルーチンを実行するたびにAddメソッドを使ってこのカウンタを増やし、ゴルーチンが終了するたびにDoneメソッドを呼び出してカウンタを減らします。

カウンタがゼロになると、Waitメソッドがブロックを解除し、プログラムの実行が続行されます。

sync.WaitGroupの使い方は非常に簡単です。

まず、sync.WaitGroupの変数を宣言し、次にゴルーチンを実行する前にAddメソッドでカウンタを増やします。

ゴルーチン内での処理が完了したら、Doneメソッドを呼び出してカウンタを減らします。

最後に、Waitメソッドを使って、すべてのゴルーチンが終了するのを待ちます。

○サンプルコード1:基本的なWaitGroupの使用方法

ここでは、sync.WaitGroupを使った基本的な例を紹介します。

この例では、3つのゴルーチンを並行して実行し、すべてのゴルーチンが完了するまでメインのゴルーチンを待機させます。

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            time.Sleep(time.Second)
            fmt.Println("ゴルーチン", i, "完了")
        }(i)
    }

    wg.Wait()
    fmt.Println("すべてのゴルーチンが完了しました")
}

このコードでは、まずsync.WaitGroupの変数wgを宣言し、forループで3つのゴルーチンを起動します。

各ゴルーチンはwg.Add(1)でカウンタを増やし、ゴルーチンの最後にdefer wg.Done()を呼び出してカウンタを減らします。

メインのゴルーチンはwg.Wait()を呼び出し、すべてのゴルーチンが完了するまで待機します。

すべてのゴルーチンが完了した後、”すべてのゴルーチンが完了しました”と表示されます。

○サンプルコード2:複数のゴルーチンを同期する

次に、複数のゴルーチンを同期するための応用例を紹介します。

この例では、異なるタスクを持つ複数のゴルーチンを同時に実行し、すべてのゴルーチンが完了するまでメインのゴルーチンを待機させます。

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()

    fmt.Printf("ワーカー%d 開始\n", id)
    time.Sleep(time.Second)
    fmt.Printf("ワーカー%d 完了\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }

    wg.Wait()
    fmt.Println("すべてのワーカーが完了しました")
}

このコードでは、worker関数を定義して、それを複数のゴルーチンで実行します。

各ワーカーはwg.Add(1)でカウンタを増やし、defer wg.Done()でカウンタを減らします。

メインのゴルーチンはwg.Wait()を呼び出し、すべてのワーカーが完了するまで待機します。

すべてのワーカーが完了した後、”すべてのワーカーが完了しました”と表示されます。

●sync.WaitGroupの応用例

sync.WaitGroupの応用例としては、エラー処理との組み合わせ、動的なゴルーチン生成と管理、複雑な並行処理の制御などが挙げられます。

これらの高度な使用方法を理解することで、Go言語における効果的なプログラミング技術を身につけることができます。

○サンプルコード3:エラー処理と組み合わせる

エラー処理をsync.WaitGroupと組み合わせることで、ゴルーチン内で発生したエラーを効果的に処理することが可能です。

下記のサンプルコードでは、複数のゴルーチンが並行して実行される際に、エラーが発生した場合にそれをキャッチしてメインのゴルーチンに通知します。

package main

import (
    "fmt"
    "sync"
    "time"
    "errors"
)

func workerWithError(id int, wg *sync.WaitGroup, errChan chan error) {
    defer wg.Done()

    time.Sleep(time.Second) // タスクの模擬
    if id == 2 { // IDが2のワーカーでエラーを発生させる
        errChan <- errors.New("エラー発生")
        return
    }
    fmt.Printf("ワーカー%d 完了\n", id)
}

func main() {
    var wg sync.WaitGroup
    errChan := make(chan error, 1)

    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go workerWithError(i, &wg, errChan)
    }

    go func() {
        wg.Wait()
        close(errChan)
    }()

    for err := range errChan {
        if err != nil {
            fmt.Println("エラー検出:", err)
            break
        }
    }
}

このコードでは、各ゴルーチンからエラー情報を受け取るためのチャンネルerrChanを使用しています。

ゴルーチン内でエラーが発生した場合、そのエラーをチャンネルに送信し、メインのゴルーチンでそれを受け取り処理します。

この方法により、複数のゴルーチン間でのエラー処理を柔軟に行うことが可能です。

○サンプルコード4:動的なゴルーチン生成と管理

複数のゴルーチンを動的に生成し、それらをsync.WaitGroupで管理する例を紹介します。

この例では、ユーザーからの入力に基づいて異なる数のゴルーチンを起動し、それらの完了を待ちます。

package main

import (
    "fmt"
    "sync"
    "time"
)

func dynamicWorker(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("ワーカー%d 開始\n", id)
    time.Sleep(time.Second)
    fmt.Printf("ワーカー%d 完了\n", id)
}

func main() {
    var wg sync.WaitGroup
    var numberOfWorkers int

    fmt.Print("ワーカーの数を入力してください: ")
    fmt.Scanln(&numberOfWorkers)

    for i := 1; i <= numberOfWorkers; i++ {
        wg.Add(1)
        go dynamicWorker(i, &wg)
    }

    wg.Wait()
    fmt.Println("すべてのワーカーが完了しました")
}

このコードでは、ユーザーが指定した数だけゴルーチンを起動し、それらがすべて完了するのをwg.Wait()で待ちます。

このように動的にゴルーチンを生成し管理することで、プログラムの柔軟性を高めることができます。

○サンプルコード5:複雑な並行処理の制御

sync.WaitGroupを使って複雑な並行処理を制御する例を紹介します。

この例では、複数の段階を持つタスクを並行して実行し、各段階の完了を同期します。

package main

import (
    "fmt"
    "sync"
    "time"
)

func complexTask(stage int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("ステージ%dのタスク開始\n", stage)
    time.Sleep(time.Second)
    fmt.Printf("ステージ%dのタスク完了\n", stage)
}

func main() {
    var wg sync.WaitGroup

    for stage := 1; stage <= 3; stage++ {
        wg.Add(1)
        go complexTask(stage, &wg)
    }

    wg.Wait()
    fmt.Println("すべてのタスクが完了しました")
}

このコードでは、3つの異なるステージのタスクを同時に実行し、それぞれのタスクが完了するまでメインのゴルーチンを待機させます。

この方法により、複数のステージを持つ複雑なタスクの完了を効果的に同期することができます。

●注意点と対処法

sync.WaitGroupを使用する際には、いくつかの注意点があり、適切な対処法を知っておくことが重要です。

特に重要なのは、デッドロックの回避とパニックのハンドリングです。

○デッドロックを避ける

sync.WaitGroupを使う際には、デッドロックに注意する必要があります。

デッドロックは、プログラムがゴルーチンの完了を永遠に待ち続ける状態を指します。

これは、例えば、必要以上にWaitGroupのカウンタを減らしてしまったり(Doneメソッドの呼び出し過多)、逆に十分に減らさなかったり(Doneメソッドの呼び出し不足)することで発生します。

デッドロックを避けるためには、AddメソッドとDoneメソッドの呼び出しを正確に行うことが重要です。

Addメソッドは、実行するゴルーチンの数だけ正確に呼び出す必要があり、Doneメソッドは各ゴルーチンの処理が終わるごとに一度だけ呼び出す必要があります。

これにより、カウンタが正確に0になり、Waitメソッドがブロックを解除することができます。

○パニックのハンドリング

ゴルーチン内でパニック(予期せぬエラー)が発生した場合、プログラム全体が停止する可能性があります。

これを防ぐために、ゴルーチン内でrecover関数を使ってパニックをキャッチし、適切に処理することが重要です。

例えば、下記のサンプルコードでは、ゴルーチン内で発生したパニックを捕捉し、メインのゴルーチンに影響を与えないようにしています。

package main

import (
    "fmt"
    "sync"
)

func safeGo(wg *sync.WaitGroup, f func()) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("パニックをキャッチ:", r)
        }
        wg.Done()
    }()
    f()
}

func main() {
    var wg sync.WaitGroup

    wg.Add(1)
    go safeGo(&wg, func() {
        // パニックを発生させる
        panic("何らかのエラー")
    })

    wg.Wait()
    fmt.Println("プログラム終了")
}

このコードでは、safeGo関数を定義し、その中でゴルーチンを実行しています。

safeGo関数内のdefer文では、recover関数を使ってパニックをキャッチし、エラーメッセージを表示しています。

これにより、メインのゴルーチンはパニックによって影響を受けずに実行を続けることができます。

●注意点と対処法

sync.WaitGroupを使う際にはいくつかの注意点があり、これらを理解し対処することで、より安全で効率的なプログラミングが可能になります。

特に重要なのが、デッドロックを避けることとパニックのハンドリングです。

○デッドロックを避ける

デッドロックは、プログラムがゴルーチンの終了を永遠に待ち続ける状態を指します。

これを防ぐためには、sync.WaitGroupを使う際には正確なカウンタ管理が不可欠です。

具体的には、すべてのゴルーチンに対してAddメソッドでカウンタを増やし、ゴルーチンが完了したらDoneメソッドでカウンタを減らす必要があります。

カウンタの増減が不均衡になると、Waitメソッドが永遠に終了しない状態、つまりデッドロックに陥る可能性があります。

デッドロックを避けるためには、下記のようなポイントに注意しましょう。

  • ゴルーチンを開始する前に、Addメソッドを呼び出してカウンタを増やす
  • ゴルーチン内での処理が終了したら、必ずDoneメソッドを呼び出してカウンタを減らす
  • ゴルーチンの開始と終了を明確に管理し、カウンタが正確に0になるようにする

○パニックのハンドリング

ゴルーチン内でパニックが発生した場合、プログラム全体が終了してしまう可能性があります。

これを防ぐために、ゴルーチン内で発生したパニックを捕捉し、適切に処理することが重要です。

Go言語では、recover関数を使用してパニックを捕捉し、プログラムの安全な継続を図ることができます。

ここではパニックのハンドリングのサンプルコードを紹介します。

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    wg.Add(1)

    go func() {
        defer wg.Done()
        defer func() {
            if r := recover(); r != nil {
                fmt.Println("Recovered in f:", r)
            }
        }()
        fmt.Println("Doing some work...")
        panic("something bad happened")
    }()

    wg.Wait()
    fmt.Println("Program completed successfully")
}

このコードでは、ゴルーチン内でpanicを呼び出していますが、defer文とrecover関数を使用することで、パニックを捕捉し、メインのゴルーチンは正常に終了します。

この方法により、一部のゴルーチンで問題が発生しても、プログラム全体の安定性を保つことができます。

●カスタマイズ方法

sync.WaitGroupの機能をさらに拡張し、カスタマイズする方法にはいくつかのアプローチがあります。

ここでは特に、カスタム同期機能の追加とパフォーマンスの最適化に焦点を当てて説明します。

○カスタム同期機能の追加

sync.WaitGroupは、基本的な同期機能を提供しますが、特定の条件下でのみゴルーチンを実行したい場合など、より複雑な同期が必要な場合があります。

これを実現するためには、sync.WaitGroupに加えて他の同期プリミティブ、例えばチャンネルやmutexを使用することが考えられます。

例えば、特定の条件を満たした時のみゴルーチンを実行するような同期処理を実装することが可能です。

下記のサンプルコードは、特定の条件下でのみゴルーチンを実行するカスタム同期機能の一例を表しています。

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    condition := true // 条件変数

    wg.Add(1)
    go func() {
        defer wg.Done()
        if condition {
            fmt.Println("条件を満たしているため、ゴルーチンを実行")
            // ゴルーチンの処理
        }
    }()

    wg.Wait()
    fmt.Println("すべてのゴルーチンが完了しました")
}

このコードでは、condition変数に基づいてゴルーチンの実行を制御しています。

このようなカスタム同期機能を利用することで、より複雑な条件に基づく並行処理の制御が可能になります。

○パフォーマンスの最適化

sync.WaitGroupを使用する際には、パフォーマンスの観点からもいくつかの最適化が可能です。

例えば、不必要なgoroutineの生成を避ける、効率的なリソース管理を行うなどが考えられます。

ゴルーチンの生成数を最小限に抑えることや、共有リソースへのアクセスを効率的に行うことは、パフォーマンスの向上に直結します。

また、sync.Poolなどのメモリプールを利用することで、頻繁に生成・破棄されるオブジェクトのコストを削減することもできます。

パフォーマンスを最適化するためには、下記のようなポイントに注意しましょう。

  • 不必要なゴルーチンの生成は避ける
  • 共有リソースへのアクセスはmutexなどを利用して効率的に行う
  • sync.Poolなどのメモリプールを適切に利用する

まとめ

この記事では、Go言語におけるsync.WaitGroupの基本から応用、注意点と対処法、さらにカスタマイズ方法に至るまでを詳しく解説しました。

sync.WaitGroupは、ゴルーチンの同期に欠かせない重要なツールです。

正確な使い方をマスターすることで、Go言語における並行処理の効率と安全性を大幅に向上させることができます。

この知識を活用して、より高度で効果的なプログラミングを目指しましょう。