Goroutinesは、Go言語における並行プログラミングの基盤となる構成要素であり、複数の論理処理を同時に実行可能にすると同時に、それら間の洗練された通信メカニズムを提供します。
プログラミングにおいて、複数の操作を同時に処理する必要がある場合、通常は並行性と並列処理という2つの概念について議論します。それぞれの意味を探ってみましょう。
並行性とは何か?
- 並行処理の世界では、1つの実行ストリームが存在しますが、同時に実行される複数のタスクが存在します。
- 例えば、複数の料理を同時に作るシェフを想像してください。シェフはメインプロセスであり、各料理は
goroutineです。シェフは一つの料理の準備を始め、その料理が調理されている間に別の料理の野菜を切る作業を始められます。このように、シェフは複数のタスクを同時に処理することで時間をより効果的に活用できます。 - Go言語では、並行処理はgoroutineとチャネルによって実現されます。goroutineについては後ほど詳しく説明します。
- 並行処理では、タスクの実行順序は保証されません。タスクは任意の順序で実行される可能性があるため、同期を確保する必要があります。
並列処理とは何か?
- 並列処理の世界では、複数の実行ストリームが存在し、各ストリームは異なるCPUコア上で実行可能です。これにより、利用可能なリソース全体をより効果的に活用できます。
- 例えば、複数のシェフがキッチンで同時に異なる料理を準備していると仮定しましょう。各シェフは独立したプロセスであり、互いに影響されずに作業できます。これにより、キッチンは利用可能なリソースを効果的に活用し、複数の料理を同時に生産できます。
- Go言語では、gomaxprocsを使用して並列処理を実現します。これは基本的に、プログラムで使用するCPUコア数を定義するものです。デフォルトではGoは利用可能な全CPUコアを使用しますが、runtimeパッケージを用いて特定の数に制限できます。
並行処理と並列処理の違いとは?
- 並行処理とは、タスクを管理・切り替えながら同時に多くのことを処理することであり、必ずしも同時に実行せずとも進捗させます。
- 並列処理とは、複数のプロセッサやコアでタスクを同時に実行することで同時に多くのことを行うことです。
- Go言語は並行処理向けに設計されており、ハードウェアリソースが許容する場合に並列処理を活用できます。
ゴルーチンとは? 🔀
goroutine(ゴルーチン)とは、Goランタイムによって管理される軽量スレッドです。これにより関数を並行して実行でき、複数の関数が互いにブロックし合うことなく同時に動作します。- ゴルーチンは
goキーワードと関数呼び出しで作成します。例:gogo runThisFunction() - これにより、メイン関数と並行して
runThisFunction()を実行する新しいgoroutineが生成されます。 - ゴルーチンが軽量である理由は、Goランタイムによって管理されるためです。つまり、自身で管理する必要がありません。
ゴルーチンはどう機能するのか? 📊
- ゴルーチンは独立したプロセスを生成するため、複数のタスクを同時に実行する、つまり並行して動作します。

goroutineに出会うと、メインプロセスから独立したプロセスがスピンアウトされ、それ以外の処理はメインプロセスで継続実行されます。別のgoroutineに遭遇すると、また別のプロセスがスピンアウトされ、これが繰り返されます。ただし、メインプロセスが終了すると、すべての goroutine も終了します。- したがって、すべてのgoroutineの実行が完了するまでメインプロセスを実行し続けたい場合、すべてのgoroutineの終了を待つためのブロッキング機構が必要です。
- このブロッキング機構を**
WaitGroup**を使用して実現する方法については、すぐに説明します。
WaitGroupとは? ⏳
WaitGroupは、すべてのgoroutineの実行が完了するまでメインプロセスをブロックする手段です。- 現在実行中のgoroutineの数を追跡する内部カウンタのようなものと考えてください。
WaitGroupのカウンタがゼロに達すると、すべてのgoroutineが実行を完了したことを意味します。 WaitGroupはGoのsyncパッケージの一部であり、goroutineを同期化する簡単な方法を提供します。
WaitGroup のメソッド ⚙️
- Add(n int):
WaitGroupのカウンタを n だけ増加させます。 - Done():
WaitGroupのカウンタを 1 だけ減少させます。 - Wait():
WaitGroupのカウンタが 0 になるまでブロックします。
defer とは何ですか?
- defer は Go のキーワードであり、関数の実行を周囲の関数が戻るまで遅延させることを可能にします。
- これは通常、
WaitGroupと組み合わせて使用され、エラーが発生した場合でもgoroutineの実行が終了した際に必ず_Done()_メソッドが呼び出されることを保証します。これにより、WaitGroupのカウンタが正しく減算され、メインプロセスが安全に終了できるようになります。
WaitGroup はどのように動作するのでしょうか?
WaitGroupを作成すると、そのカウンタは0から開始します。
go
wg := sync.WaitGroup{}
goroutineを追加する際には、Add()メソッドを呼び出してカウンタを1増加させます。
go
wg.Add(1)
goroutineの実行が終了すると、Done()メソッドを呼び出してカウンタを1減算します。
go
wg.Done()
メインプロセスはWait()メソッドを呼び出し、カウンタがゼロに達するまでブロックできます。これはすべてのgoroutineの実行が完了したことを示します。
go
wg.Wait()
ゴルーチンとWaitGroupの使用例
go
func main() {
var wg sync.WaitGroup // 1. ゴルーチン終了待ちに使用するWaitGroupを作成
wg.Add(1) // 2. 最初のgoroutineに対してWaitGroupカウンタを1増加
go func() {
defer wg.Done() // 3. このgoroutineが終了したら、WaitGroupのカウンターを1減算する
fmt.Println("1番目のgoroutineを実行中")
}() // 4. これは無名関数なので、すぐに呼び出す必要がある
wg.Add(1) // 5. 2番目のgoroutine用にWaitGroupのカウンターを1増分する
go func() {
defer wg.Done()
fmt.Println("2番目のgoroutineを実行中")
}()
wg.Wait() // 6. すべてのgoroutineの実行が完了するまで待機する
fmt.Println("すべてのgoroutineが実行を完了しました")
}
コードの説明: 📝
- ゴルーチン終了待ちに使用する
WaitGroupを作成 - 最初の
goroutineに対してWaitGroupカウンタを 1 増加 - この
goroutineが終了したら、WaitGroupのカウンターを1減算する - これは無名関数なので、すぐに呼び出す必要がある
- 2番目の
goroutine用にWaitGroupのカウンターを1増分する - すべてのgoroutineの実行が完了するまで待機する
実行順序: ⏰
- ゴルーチンの実行順序は保証されません。ゴルーチンは並行して実行され、異なるタイミングで終了する可能性があるためです。
- したがって、この例においても、プログラムを実行するたびに結果が異なる場合があります。
出力 X:
1番目のgoroutineを実行中
2番目のgoroutineを実行中
すべてのgoroutineが実行を完了しました
出力 Y:
2番目のgoroutineを実行中
1番目のgoroutineを実行中
すべてのgoroutineが実行を完了しました
まとめ: 🤝
Goroutinesは Go ランタイムが管理する軽量な「スレッド」であり、goキーワードを使用して作成されます。- 並行処理は複数のタスクを管理し、それらを切り替えることに関するものであり、並列処理は異なる CPU コア上で複数のタスクを同時に実行することに関するものです。
Goroutinesはチャネルを使用して相互に通信できます。WaitGroupは、Goroutine を同期化し、メインプロセスが終了する前にそれらの実行が完了することを保証する手段です。