見出し画像

並行処理の信頼性と正確性を確保 - GoにおけるWaitGroups入門ガイド

Goは、ゴルーチン(Go ランタイムによって管理される軽量スレッド)のおかげで、並行処理を効率的に扱う能力で知られています。Goは数千のゴルーチンを同時に実行できますが、この並行性は慎重に管理しなければならない課題をもたらします。堅牢で効率的なプログラムを確実に実現するためです。
本記事では、Goのsyncパッケージが提供する重要なツール、WaitGroupsについて探求します。すべてのゴルーチンが次の処理に進む前にタスクを完了することを保証し、不完全な操作やデータ破損などの問題を回避するための課題に取り組みます。

ただし、すべての状況でゴルーチンの完了を待つ必要があるわけではないです。場合によっては、ゴルーチンが完了を待たずに独立してタスクを実行することが良い場合もあります。例えば、ゴルーチンがログ記録やモニタリングなどのバックグラウンドタスクを実行しており、その完了が後続の操作にとって重要でない場合、WaitGroupを使用せずに実行を許可することもあります。

問題:不完全な操作

ゴルーチンを使用する際、メイン関数の実行が終了し、すべてのゴルーチンがタスクを完了する前にプログラムが終了してしまう可能性があります。これにより、一部のゴルーチンが完了まで実行される機会を得られず、不完全な操作が発生する可能性があります。
この問題を例で説明しましょう。

package main

import (
    "fmt"
)

func main() {
    messages := []string{"H", "e", "l", "l", "o"}

    for _, msg := range messages {
        go func(m string) {
            fmt.Println(m)
        }(msg)
    }

    fmt.Println("All messages printed.")
}

このコードでは、スライス内の文字が印刷される前に「All messages printed.」が表示されたり、文字の一部のみが印刷されたりする可能性があります。これは、ゴルーチンが実行される機会を得る前にプログラムが終了する可能性があるためで、不完全または一貫性のない出力につながります。

解決策:WaitGroups

WaitGroupsは、Goのsyncパッケージが提供する同期プリミティブです。これにより、プログラムの次のフェーズに進む前に、ゴルーチンのグループが実行を完了するのを待つことができ、すべてのタスクが確実に完了することを保証します。

なぜWaitGroupsを使用するのか?

time.Sleepやチャネルベースのブロッキングなどの代替手法でゴルーチンを管理することも可能ですが、これらには重大な欠点があります:

  • time.Sleep:適切な遅延を推測する必要があり、短すぎる(タスクの不完全な実行につながる)か長すぎる(時間の無駄)可能性があります。

  • ブロッキングチャネル:チャネルを使用してタスクの完了を通知することはできますが、特にゴルーチンの数が増えると、その管理が不必要に複雑になる可能性があります。

なぜWaitGroupsなのか?以下の利点があります:

  • 信頼性:次の処理に進む前にすべてのゴルーチンが確実に完了します。

  • 精度:必要な時間だけ待機し、任意の遅延を避けます。

  • シンプルさ:実装が容易で、コードの複雑さを軽減します。

WaitGroupsの動作原理

WaitGroupsの使用は簡単で、3つの主要なメソッドが関与します:

  • Add(delta int):WaitGroupsのカウンターを増加させます。各ゴルーチンを起動する前にこれを呼び出します。

  • Done():カウンターを減少させます。ゴルーチンがタスクを完了したときにこれを呼び出します。

  • Wait():カウンターがゼロになるまで実行をブロックします。これはすべてのゴルーチンが完了したことを意味します。

前の例にWaitGroupsを適用して、プログラムが終了する前にすべてのゴルーチンが完了することを確認しましょう:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup  // Create a WaitGroup

    messages := []string{"H", "e", "l", "l", "o"}

    for _, msg := range messages {
        wg.Add(1)  // Increment the counter before starting a new goroutine
        
        go func(m string) {
            defer wg.Done()  // Decrement the counter when the goroutine finishes
            fmt.Println(m)
        }(msg)
    }

    wg.Wait()  // Block until all goroutines are finished
    fmt.Println("All messages printed.")
}

このバージョンでは、WaitGroupsが「All messages printed.」を表示する前に、すべてのゴルーチンが終了するのをメイン関数が待つことを保証します。これにより、すべてのタスクが正しく完了することが保証されます。

提供された例では、「H」、「e」、「l」、「l」、「o」の文字が印刷されていますが、それらが表示される順序がスライス内の順序と一致しない可能性があります。これは、実行の並行性と並列性によるもので、ランタイムやオペレーティングシステムがCPUの可用性、タスクのスケジューリング、リソースの競合などの要因に基づいて実行順序を決定するためです。特定の順序で出力を必要とする場合、通常、そのタスクに並列のゴルーチンを使用しません。代わりに、メインスレッドでタスクを順次実行するか、順序を制御するための同期技術を使用します。ただし、これは並行性の利点を無効にしてしまいます。

結論

WaitGroupsを効果的に活用することで、不完全な操作などの一般的な並行処理の問題を防ぎ、Goプログラムがどのような環境でもスムーズかつ確実に実行されることを保証できます。


この記事は、2024年9月に弊社のフロントエンドエンジニアである Suren Sedai が執筆した内容を日本語に翻訳したものです。
英語版は、こちらからご覧いただけます。
https://articles.wesionary.team/a-beginners-guide-to-waitgroups-in-go-490f5e93b437


採用情報

私たちはプロダクト共創の仕組み化に取り組んでいます。プロダクト共創をリードするプロダクト・マネージャー、そして、私たちのビジョンを市場に届ける営業メンバーを募集しています!


開発パートナーをお探しの企業様へ

弊社は、グローバル開発のメリットを活かし、高い費用対効果と品質を両立しています。経験豊富で多様性のあるチームが、課題を正しく理解し、最適なシステムと優れた体験を実現します。業務システムの開発、新規事業の開発、業務効率化やDX化に関するお困りごと、ぜひ弊社にご相談ください。