【第十二回Go言語学習備忘録】Goroutineを使用して並列処理を実行する!

どもです。AWSから届いたパーカーの着心地が予想以上によくて四六時中着用しているsaisaiです。


本日はGoroutineを使用した並列処理に挑戦してみたので、その学習内容をまとめてみたいと思います。


並列処理位と言うワード自体聞いたことがない僕にとってはハードルが高い内容ではありましたが、頑張ってまとめてみたいと思います。

さっそく並列処理に挑戦

なにはともあれ、やってみないことには理解することはできません。

まずは今回並列処理する二つの関数を用意しました。

func goroutine(s string) {
	for i := 0; i < 5; i++ {
		fmt.Println(s)
	}
}

func normal(s string) {
	for i := 0; i < 5; i++ {
		fmt.Println(s)
	}
}

本来であれば上の関数から順番に実行されます。しかし今回は、上記の関数goroutineと関数normalを上からではなく同時に実行してみましょう。

func main() {
	go goroutine("World")
	normal("Hello")
}

上記のように片方の関数の前に"go"と付け加えることで2つの関数を同時に処理できます。出力結果はこちら!

saisai % go run main.go
Hello
Hello
Hello
Hello
Hello

なぜか関数normalの値のみが出力された結果となりました。ここにはgoroutineを使用する上で注意すべき落とし穴があります。

確かに関数goroutineも並列で処理が始まっていましたが関数normalの処理が先に終わってしまい、関数goroutineは処理半ばでmain関数が終了してしまったのです。そこで、

func main() {
	go goroutine("World")
	normal("Hello")
	time.Sleep(100 * time.Millisecond) //100ミリ秒実行を停止する。
}

上記のようにmain関数の終了を一瞬待ってやるようなコードを組み込んでみます。すると以下のような出力結果となりました。

saisai % go run main.go
Hello
Hello
Hello
Hello
Hello
World
World
World
World
World

並列処理した関数goroutineがmain関数が止まっている間に結果を返したので、上記のように無事出力されました。次に、main関数ではなく並列処理するそれぞれの関数を1回処理が行われる度に100ミリ秒待つようにしてみましょう。

func goroutine(s string) {
	for i := 0; i < 5; i++ {
		fmt.Println(s)
		time.Sleep(100 * time.Millisecond)
	}
}

func normal(s string) {
	for i := 0; i < 5; i++ {
		fmt.Println(s)
		time.Sleep(100 * time.Millisecond)
	}
}

すると結果は以下のようになります。

saisai % go run main.go
Hello
World
World
Hello
Hello
World
World
Hello
Hello
World

それぞれが5回繰り返される処理のうち一回ごとに待ち時間が発生しているため、それぞれの関数の処理結果が処理終了次順々に表示されています。並列処理をしているということがとてもわかりやすい一例かと思います。

sync.WaitGroupを使用する

このように並列処理をする場合は、どちらかの処理が先に終了しないように工夫してあげる必要があります。しかし、並列処理をしようとする度にそれぞれの処理の実行にかかる時間を予測し、一時停止する必要があるのでしょうか。


明示的に、"並列処理している方も終わるまで待ってね"と指定できれば一時停止を考慮する必要はありませんよね。


そこで便利なのがsyncパッケージのWaitGroupメソッドです。以降"sync.WaitGroup"と表記します。こちらを並列処理している方に設定することで、処理が終了するまで関数の終了を待機してくれます。例えばmain関数内で以下のように定義してみます。

func main() {
	var wg sync.WaitGroup //wgをsync.WaitGroupメソッドと定義
	wg.Add(1) //sync.WaitGroupを設定した処理が1つ終わるまで待つよう設定
	go goroutine("World", &wg) //wg(sync.WaitGroupメソッド)も引数として並列処理開始
	normal("Hello")
}

並列処理する関数側では以下のように定義します。

func goroutine(s string, wg *sync.WaitGroup) {
	for i := 0; i < 5; i++ {
		time.Sleep(100 * time.Millisecond)
		fmt.Println(s)
	}
 wg.Done()
}

引数としてsync.WaitGroupのポインタをとります。処理の終了は"wg.Done()"が呼び出されることで判定されます。こちらを指定しないと"wg.Add(1)"の1に延々と到達せずにエラーを出してしまいます。ちなみに"wg.Done()"ですが、以下のように"defer"を使用した遅延処理で先に定義しておくこともできます。

func goroutine(s string, wg *sync.WaitGroup) {
	defer wg.Done()
	for i := 0; i < 5; i++ {
		time.Sleep(100 * time.Millisecond)
		fmt.Println(s)
	}
}

こちらの方が指定漏れを防ぐことが出来て良さそうですね。これでmain関数は"wg.Done()"を一回呼び出されたことを確認してから終了するようになりましたので、並列処理が置き去りになるということはなくなりました。


ここまでの内容を踏まえて、goroutineとsync.WaitGroupを利用して3つの関数を並行処理するコードを作成してみました。

package main

import (
	"fmt"
	"sync"
	"time"
)

func goroutine(s string, wg *sync.WaitGroup) {
	defer wg.Done()
	for i := 0; i < 5; i++ {
		time.Sleep(100 * time.Millisecond)
		fmt.Println(s)
	}
}

func goroutine2(s string, wg *sync.WaitGroup) {
	defer wg.Done()
	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() {
	var wg sync.WaitGroup
	wg.Add(2)
	go goroutine("blue", &wg)
	go goroutine2("yellow", &wg)
	normal("red")
}

実行結果は以下の通りです。うまく全ての関数の結果が出力されていますね。

saisai % go run main.go
red
yellow
blue
blue
yellow
red
red
blue
yellow
yellow
blue
red
yellow
blue
red

ひとこと

今回はGoroutineを使用した並列処理についてまとめてみました。決済周りの処理などシビアな部分で使用されることも多々あるらしく、今回の学習を通して確実に全ての処理が実行されているかを意識しながら実装していくことが大切だと感じました。


僕の仕事も並列処理してさっさと終わらせたいな。


ここまで読んでいただきありがとうございました!


-saisai-

Golang

Posted by CY