Go言語は並列処理に必要な機能を標準でサポートしています。
その並列処理を実現する機能として、ゴルーチンやチャネルがあります。
他の言語では、マルチスレッドやイベント駆動などと呼ばれることも。
この記事では以下を解説していきます。
- 並列処理と並行処理の違いとは
- ゴルーチンとは
- ゴルーチンについての簡単な解説
さっそく見ていきましょう。
並列処理と並行処理の違いとは
一般に並列処理と並行処理の違いは以下の通りです。
並列処理(Parallel)
- たくさんの計算を同時に実行すること
- 複数のプロセッサやコアを使って、仕事を同時に行うこと
並行処理(Concurrent)
- プロセス同士が独立して実行されること
- CPU数・コア数の限界を超えて複数の仕事が同時に実行されること
並行処理では一度にたくさんのことを扱って、並列処理では一度にたくさんのことを実行します。
ゴルーチンとは
Goでは、スレッドよりも小さい単位で処理単位であるゴルーチンが並行して動作するように実装されています。
ゴルーチンとは、他のコードに対して並行に実行している関数のことです。
「ゴルーチン」は、独立して動作する実行単位であり、OSが管理するスレッドに割り当てられて動作します。
「ゴルーチン」として並列実行させたい処理は、関数として実装します。関数は、「ゴルーチン」だからと言って特別な実装は必要ありません。
参考: はじめてのGo言語
ゴルーチンは、任意の関数をGoのランタイムの中で並行に実行します。
つまりmain()関数などもゴルーチンの1つです。
ゴルーチンについての簡単な解説
ゴルーチンの使い方は非常に簡単で、並行で実行したい関数の前にgoと書いて呼ぶのみです。
func main() {
go 関数名(引数)
}
ここからは具体的なゴルーチンの使い方を見ていきます。
以下のコードは、Udemyの「現役シリコンバレーエンジニアが教えるGo入門 + 応用でビットコインのシストレFintechアプリの開発」より引用しました。
1. 並列実行するコード
package main
import (
"fmt"
"time"
)
func goroutine(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func normal(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go goroutine("world")
normal("hello")
}
// 出力
hello
world
hello
world
hello
hello
world
hello
hello
world
このコードではゴルーチンを使って、関数goroutineを並列で実行しています。
並列に実行させるには、先ほど述べたように関数の前にgoとつけて呼ぶだけです。
ここで、「go」という宣言を消すと、以下のように出力されます。
world
world
world
world
world
hello
hello
hello
hello
hello
2.timeを消して呼んでみる
package main
import (
"fmt"
"time"
)
func goroutine(s string) {
for i := 0; i < 5; i++ {
//time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func normal(s string) {
for i := 0; i < 5; i++ {
//time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go goroutine("world")
normal("hello")
}
// 出力
hello
hello
hello
hello
hello
このコードではnormal関数の処理が先に終了するため、helloのみが出力されて終わります。
goで関数goroutineのスレッドを生成していますが、実行する前に終了してしまっています。
試しにここで、main関数のnormal(“hello”)の下に以下を加えてみると1のコードと似た結果が表示されます。
3. Sync.Waitを使ったコード
package main
import (
"fmt"
"time"
)
func goroutine(s string, wg *sync.WaitGroup) { //sync.WaitGroupのポインタを引数にする
for i := 0; i < 5; i++ {
//time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
wg.Done() //wg.Done()で終了を知らせる(呼び忘れるとエラー)
}
func normal(s string) {
for i := 0; i < 5; i++ {
//time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
var wg sync.WaitGroup //sync.WaitGroupを変数に格納
wg.Add(1) //並列処理するゴルーチンをカウントする
go goroutine("world", &wg) //引数にwgのポインタを渡す
normal("hello")
wg.Wait() //wgがDoneするまで待機する
}
// 出力
hello
hello
hello
hello
hello
1, 2でのコードでは、ゴルーチンとして並列に実行した処理が終わるように時間に気を配る必要があります。ですが、そんなことをするのは面倒です。
その面倒を解消するのが、「synw.WatiGroup」。
sync.WaitGroupは複数のgoroutineの完了を待つための値です。
関数goroutineの中でwg.Done()としている処理は、呼び忘れるとエラーになります。なぜかというと、wg.Add(1)として、1つ並列での処理を待つようにしているからです。
DoneとAddの数は同じになるべきです。
sync.WaitGroupを使うと複数のgoroutineが終わるのを待つことができて、それらのgoroutineの処理が完了した後に処理を書きたい時に便利です。
終わりに
ゴルーチンについて簡単に解説しました。
Go言語での重要な機能なのでより深く理解していきたいですね。
コードでも参考にしたUdemyの講座では、分かりやすくGoについて学べました。
>> 現役シリコンバレーエンジニアが教えるGo入門 + 応用でビットコインのシストレFintechアプリの開発
参考文献
スターティングGo言語
はじめてのGo言語
現役シリコンバレーエンジニアが教えるGo入門
Goの並行処理 -基本のキ-