最近少しづつGo言語について学んでいまして、その中でも並列処理についてある程度、基本を理解できたかなと思うので、整理する意味も込めて記事にしていきたいと思います。
並列処理とは何か?
並列処理とは独立した処理を複数のプロセッサを使用して、複数コアを持つハードウェアのパフォーマンスを引き出す(つまり高速化するための)技術です。
高速化が必要な計算処理系や画像処理などにおいて使用されます。
JavaScriptの場合、promise.allメソッドなどを使用して実現が可能です。
並列処理と並行処理の比較
並行処理と並列処理はPCのCPUがどのようにタスクを処理するのかの方法の違いになります。
並行処理(Concurrency)
並列処理は、複数のスレッドを共通の期間内で実行する能力のことを指します。
複数のタスク間を切り替えることによって、タスクを同時にやっているように見えますが、実際はある一つの地点において、1つのタスクしか行いません。
Oナノ秒毎に高速にタスクを切り替えている為、同時に実行しているように見えます。
並列処理(Parallelism)
複数のスレッドを同時に実行する能力のことを指します。並列実行の場合、システム自体にマルチコアプロセッサ(2つ以上のプロセッサコアを単一のICチップに集積したもの)が必要です。
マルチコアプシステムでない場合は並列処理を実行することはできません。並行処理は1つのコアでタスクを切り替えて1つの処理を行う為、コアに負担がかかる作業ですが、並列処理はそれぞれのコアがそれぞれの処理を行います。
ちなみに、同期せずに処理を実行できる非同期処理がありますが、これは実際に実行している処理は常に一つだけであり、並列処理とは異なる概念です。
Goの並列処理に必要なもの
Goの並列処理を実現するために必要なものは以下の3つです。
- Goroutine
- Sync.WaitGroup
- Channelによる実行数の制御
Goroutine
Goroutineは、Goのランタイムに管理される軽量なスレッドです。
メモリ消費が少なくカーネルスレッドの場合、スレッド間のメモリ保護の役割をするGuard pageスペースを含めて1MBほど必要になりますが、Goroutineの場合は2KBのスタックのみとなります。
またカーネルスレッドはOSからリソースを要請し、作業が終了したらリソースを戻す時間が必要になりますが、GoroutineはGo Runtimeから生成~終了するという特徴があります。
メインで実行される処理(スレッド)とは別に独立したスレッドで処理を行うことが可能です。
例えば、以下のようなfor文を記載してみます。
スリープ関数により、1秒毎に結果が出力されるようにします。
for i := 0; i < 5; i++ { // for文
i := i
func() {
time.Sleep(1 * time.Second) // スリープ関数(1秒毎)
fmt.Printf("%d\n", i) // 出力
}()
}
fmt.Printf("end\n") // 出力
出力結果は以下の通りです。
➜ go go run main.go
0
1
2
3
4
end
Goroutineの実装方法自体は簡単で、Goを関数に記載するだけです。
for i := 0; i < 5; i++ { // for文
i := i
go func() { // Goroutine
time.Sleep(1 * time.Second) // スリープ関数(約1秒毎)
fmt.Printf("%d\n", i) // 出力
}()
}
fmt.Printf("end\n") // 出力
ですがこの場合、結果ができる前にプログラムが終了しendのみが出力されてしまいます。
➜ go go run main.go
end
これは、メインの処理が終了した場合、その他のGoroutineの終了を待つことなくプログラムが終了するという特徴によるものです。
起動は簡単ですが、Goroutineを終了するのを待たせる為に、別の仕組みが必要になります。
それが次に記述するSync.WaitGroupです。
Sync.WaitGroup
Sync.WaitGroupでは最初に構造体wgを用意して、カウンターを設定します。
Sync.waitGroupは、内部にカウンタを持っており、初期化時点でカウンターの値は0となります。
- wg Sync.WaitGroup構造体を用意
- wg.Add() Sync.WaitGroup内部をカウント
- wg.Done() 処理が終了するごとにSync.WaitGroup内部をディスカウント
- wg.Wait() 内部カウンタが0になるまでメインのGoroutineをブロック
var wg sync.WaitGroup // sync.WaitGroup構造体wgを用意
for i := 0; i < 5; i++ { // for文
i := i
wg.Add(1) // Goroutineを呼ぶ前にWaitGroupを実行(wgの内部カウンタの値を+1)。
go func() { // Goroutine
time.Sleep(1 * time.Second) // スリープ関数(約1秒毎)
fmt.Printf("%d\n", i) // 出力
wg.Done() // wgの内部カウンタの値を-1するように設定
}()
}
wg.Wait() // 内部カウンタが0になるまでメインのGoroutineをブロックして待つ
fmt.Printf("end\n") // 出力
これでメインの処理を待たせることができるようになりました。
出力結果(並列処理が実行)
➜ go go run main.go
0
1
2
3
4
end
図にすると以下のようなイメージです。
Channelによる実行数の制御
最後にChannelによる実行数の制御について述べていきます。
Goroutineが無限に実行できてしまうと、問題となるため同時に実行するGoroutineの実行数を制御します。
num := flag.Int("num", 3, "num")
var wg sync.WaitGroup // sync.WaitGroup構造体wgを用意
limit :=make(chan bool, *num) // チャンネル数の制御
for i := 0; i < 5; i++ { // for文
i := i
wg.Add(1) // goルーチンを呼ぶ前にWaitGroupを実行(wgの内部カウンタの値を+1)。
limit <- true // チャンネル数の制御
go func() { // goルーチン
time.Sleep(1 * time.Second) // スリープ関数(約1秒毎)
fmt.Printf("%d\n", i) // 出力
wg.Done() // wgの内部カウンタの値を-1するように設定
<-limit
}()
}
wg.Wait() // 内部カウンタが0になるまでメインゴールーチンをブロックして待つ
fmt.Printf("end\n") // 出力
これにより3つずつ実行されるようになります。
➜ go go run main.go
1
0
2
今回はここまでとなります。
最後までお読みいただきありがとうございました。
参考文献