goroutineによる並行処理(go, sync.WaitGroup)

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コアを占有できず 並行処理(論理的に順不同で同時実行) になります。

参考

わくわくBank.
技術系の記事を中心に、役に立つと思ったこと、整理したい情報などを掲載しています。