Goroutine の並行処理によって並列処理を実現する
國忠です。
Sprocketではデータを分析するためにデータベースやWeb APIに対して大量のリクエストを発行しており、逐次処理ではビジネスニーズに対応できる速度を出すことができません。
そのため、複数のスレッドを使い並列に処理をしたい、というモチベーションが出てきます。
OSスレッドを使う方法が並列処理のよく知られる手法ですが、最近はGoroutineなどのユーザー空間スレッドを使う手法もでてきました。
しかし、Go言語のドキュメント では、Goroutineは並列性(Parallelism)ではなく並行性(Concurrency)の項目に書かれています。
簡単に定義の違いを説明すると並行性はコードの性質であり、並列性は動作しているプログラムの性質です。
図で解説すると次のようになります。(Mozzilaのエンジニアである Nikolay Grozevさんのブログ の画像を日本語にさせていただきました。)
つまり、Goroutineによって並行処理として書かれていてもGoランタイムや動作するコンピューティング環境によっては、並列に処理できないということです。
ですので、今回はGo言語1.15のランタイムと8コア16スレッドのコンピューティング環境を使い、Goroutineで並列処理ができるかを検証しました。
Goroutine とは
Go言語の作者の一人であるRob Pike氏による定義 と How Goroutines Work に詳しい記述があります。
要約するとGoroutineには下記の特徴があります。
- OSスレッドではなく、ユーザー空間スレッドである。
- メモリ使用量がOSスレッドに対して、500倍ほど有利である。
- コンテキストスイッチにかかる時間が、OSスレッドに対して有利である。
- 生成と破棄にかかる時間が、OSスレッドに対して有利である。
- Goroutineは1つのOSスレッドに対して複数配置され、必要に応じて動的に利用するOSスレッドを増やす。
検証方法
「約300MBのファイルのハッシュを計算するタスクを30回行う」というプログラムを下記の4パターン作成しました。
それらの実行結果を元に並列処理かどうかを判定します。
runtimeの GOMAXPROCS によって実行時に使用したいCPUの最大数を設定可能であるため、それを利用して並列数の変化を試みます。
手法 | GOMAXPROCS |
---|---|
通常のfor文 | 16 |
Goroutine | 1 |
Goroutine | 4 |
Goroutine | 16 |
ソースコードは以下のGistに配置しました。
https://gist.github.com/rysk92/0fd45a42de72c6e99cec2aaea25a42e7
package main
import (
"crypto/sha256"
"fmt"
"io"
"log"
"os"
"runtime"
"runtime/trace"
)
func main() {
f, err := os.Create("runtime.trace")
if err != nil {
fmt.Println(err)
}
trace.Start(f)
defer trace.Stop()
byForLoop()
byGoroutine(1)
byGoroutine(4)
byGoroutine(runtime.NumCPU())
}
func getHash() string {
f, err := os.Open("./golang/debian-10.8.0-amd64-netinst.iso")
if err != nil {
log.Fatal(err)
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
log.Fatal(err)
}
hash := fmt.Sprintf("%x", h.Sum(nil))
return hash
}
func getHashByChannel(c chan string) {
c <- getHash()
}
func byForLoop() {
loopCount := 30
for i := 0; i < loopCount; i++ {
hash := getHash()
fmt.Println(hash)
}
}
func byGoroutine(numberOfCPU int) {
runtime.GOMAXPROCS(numberOfCPU)
c := make(chan string)
loopCount := 30
for i := 0; i < loopCount; i++ {
go getHashByChannel(c)
}
for i := 0; i < loopCount; i++ {
hash := <-c
fmt.Println(hash)
}
}
検証結果
runtime/trace で出力したファイルに対して go tool trace を実行することにより、下記の画像のようにブラウザを用いてプロファイルを表示できます。
縦軸がスレッド数、横軸が経過時間です。
GOMAXPROCS=2
以上の値、かつGoroutineを指定した場合、期待通り複数のスレッドを使用した並列処理を実現できます。
逐次処理およびGOMAXPROCS=1
の場合に比べ、実行環境の最大値である16まで並列に処理したGoroutineは、同じ量の仕事を約1/5の時間で処理できました。
まとめ
Goroutineを使用したプログラムに対してCPUの使用数を指定することにより、並列処理が行えていることを確認しました。
Discussion