Goで並列処理する超入門

Go(Golang)はマルチプロセッサ・マルチコアを有効に使うにはうってつけの言語です。今回は並列処理する際の超入門的な解説をします。

並列処理をするには、まずはここから始めると良いと思います。

並列処理しないコード

次のコードをコンパイルして実行してみます。

package main

import (
	"fmt"
	"time"
)

func foo() {
	for i := 0; i < 10; i++ {
		fmt.Println("foo")
		time.Sleep(100 * time.Microsecond)
	}
}

func bar() {
	for i := 0; i < 10; i++ {
		fmt.Println("bar")
		time.Sleep(100 * time.Microsecond)
	}
}

func main() {
	fmt.Println("Start main")
	foo()
	bar()
	fmt.Println("Finish main")
}

実行結果は次の通りです。当然ですがfoo()を実行した後にbar()が実行されます。

Start main
foo
foo
foo
foo
foo
foo
foo
foo
foo
foo
bar
bar
bar
bar
bar
bar
bar
bar
bar
bar
Finish main

並列処理するコード

では次にgoルーチンを使って並列処理してみます。並列処理させるには「go」を関数の呼びだし前に付けるだけです。

package main

import (
	"fmt"
	"time"
)

func foo() {
	for i := 0; i < 10; i++ {
		fmt.Println("foo")
		time.Sleep(100 * time.Microsecond)
	}
}

func bar() {
	for i := 0; i < 10; i++ {
		fmt.Println("bar")
		time.Sleep(100 * time.Microsecond)
	}
}

func main() {
	fmt.Println("Start main")
	go foo()
	go bar()
	fmt.Println("Finish main")
}

このコードをコンパイルして実行してみますが、何だかおかしいですね。foo()とbar()の実行結果が表示されません。

Start main
Finish main

この理由は簡単です。これはfoo()とbar()が並列処理している間、メインのルーチンも動いているのでメインルーチンが終わりに達してプログラム全体が終了しているためです。

ですからfoo()とbar()が終わるまで待たなければいけません。プログラムが終わる前に1秒待機してみましょう。

package main

import (
	"fmt"
	"time"
)

func foo() {
	for i := 0; i < 10; i++ {
		fmt.Println("foo")
		time.Sleep(100 * time.Microsecond)
	}
}

func bar() {
	for i := 0; i < 10; i++ {
		fmt.Println("bar")
		time.Sleep(100 * time.Microsecond)
	}
}

func main() {
	fmt.Println("Start main")
	go foo()
	go bar()
	time.Sleep(1 * time.Second)
	fmt.Println("Finish main")
}

このコードをコンパイルして実行するとfoo()とbar()の実行結果が表示されました。並列処理している様子が見て取れます。

Start main
bar
foo
foo
bar
bar
foo
foo
bar
bar
foo
foo
bar
bar
foo
foo
bar
bar
foo
foo
bar
Finish main

しかし、この方法は非現実的です。foo()とbar()の処理が1秒で確実に終わるならば良いのですが、実際には何秒かかるのか実行時間は事前に分からないでしょう。

そこでfoo()とbar()が終了するまで待機する仕組みを取り入れます。この方法にはWaitGroupを使う方法とchannelを使う方法がありますが、今回はより簡単なWaitGroupを使います。

channelを使うとgoルーチン間で通信させたりブロックができるのですが、慣れないうちはデッドロックさせてしまうなど意外と難しいので今回のように単純に処理が終わるのを待つだけならばWaitGroupがおすすめです。

WaitGroupでfoo()とbar()の終了を待つ

WaitGroupを使うには、最初に実行するGoルーチンの数を登録して、Goルーチン内で処理が完了したら終了を伝えます。これによって登録した数のGoルーチンがすべて完了したのかチェックしています。

package main

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

func foo(wg *sync.WaitGroup) {
	// すべての処理が完了したら終了を伝える
	defer wg.Done()

	for i := 0; i < 10; i++ {
		fmt.Println("foo")
		time.Sleep(100 * time.Microsecond)
	}
}

func bar(wg *sync.WaitGroup) {
	// すべての処理が完了したら終了を伝える
	defer wg.Done()

	for i := 0; i < 10; i++ {
		fmt.Println("bar")
		time.Sleep(100 * time.Microsecond)
	}
}

func main() {
	var wg sync.WaitGroup

	fmt.Println("Start main")

	// 2個のGoルーチンを実行する
	wg.Add(2)
	go foo(&wg)
	go bar(&wg)

	// すべてのGoルーチンが完了するまで待つ
	wg.Wait()
	fmt.Println("Finish main")
}

注意しないといけないのは、foo()とbar()にはそれぞれ「wg」のポインタ(参照渡し)を引数とする点です。このコードをコンパイルして実行してみましょう。

Start main
bar
foo
foo
bar
bar
foo
foo
bar
bar
foo
foo
bar
bar
foo
foo
bar
bar
foo
foo
bar
Finish main

ご覧のとおり、foo()とbar()の処理が完了するまで待機してからプログラムが終了しています。

まとめ

Goは並列処理を高度に抽象化しているので、並列処理に興味があるならば是非Goを使ってみて欲しいです。仕事の現場でも役に立つツールを簡単に作成できるので、Goを使えるように勉強するのは価値があると言えるでしょう。

この記事は役に立ちましたか?

もし参考になりましたら、下記のボタンで教えてください。

コメント

この記事へのコメントはありません。