goの特徴である「goroutine」による並行処理について確認します。直列処理のコードを「goroutine」を利用して並行処理にして、処理時間の変化などを確認します。
目次
動作確認環境
CPUが2つです。
# lscpu | grep -e CPU -e Thread
CPU op-mode(s): 32-bit, 64-bit
CPU(s): 2
On-line CPU(s) list: 0,1
Thread(s) per core: 1
CPU family: 6
Model name: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
CPU MHz: 2400.000
直列処理
コード
まずはgoroutineを利用せずに、直列で実行するコードについて確認します。
package main
import (
"fmt"
"runtime"
"time"
)
func getMinutesAndSeconds() string {
return time.Now().Format("04:05")
}
// 5秒sleep
func test(n int) {
fmt.Printf("[Start]\t%v\t[%v]\n", n, getMinutesAndSeconds())
time.Sleep(5 * time.Second)
fmt.Printf("[Done]\t%v\t[%v]\n", n, getMinutesAndSeconds())
}
func main() {
s := time.Now()
fmt.Printf("[Start]\t\t[%v]\n", getMinutesAndSeconds())
for i := 0; i < 2; i++ {
test(i)
}
fmt.Printf("CPUのコア数: %v\n", runtime.NumCPU())
fmt.Printf("Goroutineの数: %v\n", runtime.NumGoroutine())
fmt.Printf("[Done]\t\t[%v]\n", getMinutesAndSeconds())
e := time.Now()
fmt.Printf("処理秒数: %v\n", e.Sub(s).Round(time.Second))
}
実行結果
# go run main.go
[Start] [45:52]
[Start] 0 [45:52]
[Done] 0 [45:57]
[Start] 1 [45:57]
[Done] 1 [46:02]
CPUのコア数: 2
Goroutineの数: 1
[Done] [46:02]
処理秒数: 10s
goroutineを使ってみる
( NG編 )
コード
test関数の実行を go test(i)
とすることで、goroutineとして実行します。
package main
import (
"fmt"
"runtime"
"time"
)
func getMinutesAndSeconds() string {
return time.Now().Format("04:05")
}
// 5秒sleep
func test(n int) {
fmt.Printf("[Start]\t%v\t[%v]\n", n, getMinutesAndSeconds())
time.Sleep(5 * time.Second)
fmt.Printf("[Done]\t%v\t[%v]\n", n, getMinutesAndSeconds())
}
func main() {
s := time.Now()
fmt.Printf("[Start]\t\t[%v]\n", getMinutesAndSeconds())
for i := 0; i < 2; i++ {
go test(i)
}
fmt.Printf("CPUのコア数: %v\n", runtime.NumCPU())
fmt.Printf("Goroutineの数: %v\n", runtime.NumGoroutine())
fmt.Printf("[Done]\t\t[%v]\n", getMinutesAndSeconds())
e := time.Now()
fmt.Printf("処理秒数: %v\n", e.Sub(s).Round(time.Second))
}
実行結果
# go run main.go
[Start] [46:41]
CPUのコア数: 2
Goroutineの数: 3
[Done] [46:41]
処理秒数: 0s
Goroutineの数: 3
となりましたが、test関数の出力が表示されていません。goroutineの実行完了前にmainの処理が終了したためです。
goroutineを使ってみる
( sync.WaitGroupで処理が終わるのを待つ )
コード
今度は、sync.WaitGroup
を利用します。goroutineの実行完了を待った上で、mainの処理を終了するようにします。
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func getMinutesAndSeconds() string {
return time.Now().Format("04:05")
}
// 5秒sleep
func test(n int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("[Start]\t%v\t[%v]\n", n, getMinutesAndSeconds())
time.Sleep(5 * time.Second)
fmt.Printf("[Done]\t%v\t[%v]\n", n, getMinutesAndSeconds())
}
func main() {
var wg sync.WaitGroup
s := time.Now()
fmt.Printf("[Start]\t\t[%v]\n", getMinutesAndSeconds())
for i := 0; i < 2; i++ {
wg.Add(1)
go test(i, &wg)
}
fmt.Printf("CPUのコア数: %v\n", runtime.NumCPU())
fmt.Printf("Goroutineの数: %v\n", runtime.NumGoroutine())
wg.Wait()
fmt.Printf("[Done]\t\t[%v]\n", getMinutesAndSeconds())
e := time.Now()
fmt.Printf("処理秒数: %v\n", e.Sub(s).Round(time.Second))
}
- goroutine実行前に、
wg.Add(1)
を行っています。 - main処理では
wg.Wait()
でgoroutineの実行完了(wg.Done()
の実行 )を待っています。 - goroutineとして実行する関数内で処理が終了したタイミングで
wg.Done()
をしています。
実行結果( Goroutineの数: 3 )
# go run main.go
[Start] [43:40]
CPUのコア数: 2
Goroutineの数: 3
[Start] 0 [43:40]
[Start] 1 [43:40]
[Done] 1 [43:45]
[Done] 0 [43:45]
[Done] [43:45]
処理秒数: 5s
直列で実行したときの処理時間は10秒だったので、処理時間を半分にすることができました。
実行結果( Goroutineの数: 11 )
以下のようにtest関数の呼び出しを10回に変更して実行してみます。
for i := 0; i < 10; i++ {
wg.Add(1)
go test(i, &wg)
}
# go run main.go
[Start] [48:31]
CPUのコア数: 2
Goroutineの数: 11
[Start] 9 [48:31]
[Start] 0 [48:31]
[Start] 5 [48:31]
[Start] 2 [48:31]
[Start] 3 [48:31]
[Start] 4 [48:31]
[Start] 1 [48:31]
[Start] 7 [48:31]
[Start] 6 [48:31]
[Start] 8 [48:31]
[Done] 7 [48:36]
[Done] 8 [48:36]
[Done] 9 [48:36]
[Done] 0 [48:36]
[Done] 1 [48:36]
[Done] 5 [48:36]
[Done] 3 [48:36]
[Done] 2 [48:36]
[Done] 4 [48:36]
[Done] 6 [48:36]
[Done] [48:36]
処理秒数: 5s
test関数の処理はsleepしているだけなので、処理秒数は変わりませんね。
test関数の処理を変更
コード
CPUコアが稼働し続けるように、test関数の処理を変更して動作確認します。
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func getMinutesAndSeconds() string {
return time.Now().Format("04:05")
}
// 適当な重めの処理
func test(n int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("[Start]\t%v\t[%v]\n", n, getMinutesAndSeconds())
var x int64
for i := 0; i < 10000000000; i++ {
if i%2 == 0 {
x = x + int64(i)
} else {
x = x - int64(i)
}
}
fmt.Printf("[Done]\t%v\t[%v]\t[%v]\n", n, getMinutesAndSeconds(), x)
}
func main() {
var wg sync.WaitGroup
s := time.Now()
fmt.Printf("[Start]\t\t[%v]\n", getMinutesAndSeconds())
for i := 0; i < 2; i++ {
wg.Add(1)
go test(i, &wg)
}
fmt.Printf("CPUのコア数: %v\n", runtime.NumCPU())
fmt.Printf("Goroutineの数: %v\n", runtime.NumGoroutine())
wg.Wait()
fmt.Printf("[Done]\t\t[%v]\n", getMinutesAndSeconds())
e := time.Now()
fmt.Printf("処理秒数: %v\n", e.Sub(s).Round(time.Second))
}
実行結果
( Goroutineの数: 1, test関数呼び出し: 2 )
以下のように、go test(i, &wg)
ではなく、 test(i, &wg)
にして動作確認します。
for i := 0; i < 2; i++ {
wg.Add(1)
test(i, &wg)
}
# go run main.go
[Start] [53:39]
[Start] 0 [53:39]
[Done] 0 [53:44] [-5000000000]
[Start] 1 [53:44]
[Done] 1 [53:49] [-5000000000]
CPUのコア数: 2
Goroutineの数: 1
[Done] [53:49]
処理秒数: 11s
直列実行で11秒かかっています。
実行結果
( Goroutineの数: 3, test関数呼び出し: 2 )
以下のようにして動作確認します。
for i := 0; i < 2; i++ {
wg.Add(1)
go test(i, &wg)
}
# go run main.go
[Start] [51:19]
CPUのコア数: 2
Goroutineの数: 3
[Start] 1 [51:19]
[Start] 0 [51:19]
[Done] 1 [51:25] [-5000000000]
[Done] 0 [51:25] [-5000000000]
[Done] [51:25]
処理秒数: 6s
CPUのコア数は2なので、ほぼ 並列処理(物理的に同時実行)
で実行できていそうです。直列実行の半分の時間で完了しました。
実行結果
( Goroutineの数: 4, test関数呼び出し: 3 )
for i := 0; i < 3; i++ {
wg.Add(1)
go test(i, &wg)
}
# go run main.go
[Start] [58:08]
CPUのコア数: 2
Goroutineの数: 4
[Start] 2 [58:08]
[Start] 0 [58:08]
[Start] 1 [58:08]
[Done] 1 [58:16] [-5000000000]
[Done] 2 [58:16] [-5000000000]
[Done] 0 [58:16] [-5000000000]
[Done] [58:16]
処理秒数: 8s
CPUのコア数は2なので、CPUコアを占有できず 並行処理(論理的に順不同で同時実行)
になります。