はじめに
Go言語が採用される一つの理由である、"並行"実行が簡単に書くことができるということです。
そんなGo言語の"並行"実行を叶えるgoroutine(&& channel)をまとめようと思います。
(自分の勉強も含めて)
実行環境
version | |
---|---|
OS | Mac HighSierra 10.13.1 |
Go | 1.9.2 |
goroutine
複数のタスクが同時に実行される関数のことです。
goroutineはスレッドを使用しているので、スレッドに似ているものを想定していただいて構いませんが、
スレッドよりも軽量なのでより多くのgoroutineを生成することができます。
channel
並行で実行されているgoroutine間の通信のためにあります。
ある1つのgoroutineが別のgoroutineに何かを渡したい場合に使用します。
並行と並列の違い
Go言語がgoroutineとして提供しているのは"並行"処理です。(並列処理も可能)
"並行"処理と"並列"処理は混同しがちなので、その2つの違いを明確にしておきます。
Go言語の偉大なる開発者の一人であるRob Pike氏も動画で触れています。動画のリンク
複数のタスクを同時に実行することに違いはありません。
並行処理は1つのリソースを使用して、順不同もしくは同時にタスクが実行されることです。
並列処理は複数を使用して、同時に複数のタスクが実行されることです。
goroutineの基本的な使用方法
使用方法は(無名)関数の前にgo
キーワードをつけて記述するだけです。
package main
import (
"fmt"
"time"
)
func printNumbers(begin, end int) {
for i := begin; i < end; i++ {
fmt.Println(i)
}
}
func main() {
fmt.Println("--- start ---")
printNumbers(1, 5)
go printNumbers(6, 10) // goroutine1
go printNumbers(11, 15) // goroutine2
time.Sleep(100 * time.Millisecond) // 0.1秒遅延
fmt.Println("--- finish ---")
}
※ プログラムの処理が非同期処理も早く終了してしまって結果が出力されないので0.1秒遅延させてます。
--- start ---
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
--- finish ---
printNumbers(1, 5)
こちらは普通に関数呼び出しをされているので実行される順番は変わりませんが、
go printNumbers(6, 10)
とgo printNumbers(11, 15)
はgoroutineを使用して関数を呼び出しているので、
何度か実行していると以下のような実行結果になることもあります。
--- start ---
1
2
3
4
5
11
12
13
14
15
6
7
8
9
10
--- finish ---
今回の例だとgoroutineを使って呼び出している関数の処理がとても軽量なので、
関数呼び出しごとのまとまった出力が得られますが、以下のように呼び出す関数にも遅延(処理時間を増やす)を追加すれば、また結果が変わります。
package main
import (
"fmt"
"time"
)
func printNumbers(begin, end int) {
for i := begin; i <= end; i++ {
time.Sleep(1 * time.Millisecond) // 遅延を追加
fmt.Println(i)
}
}
func main() {
fmt.Println("--- start ---")
printNumbers(1, 5)
go printNumbers(6, 10) // goroutine1
go printNumbers(11, 15) // goroutine2
time.Sleep(100 * time.Millisecond) // 0.1秒遅延
fmt.Println("--- finish ---")
}
--- start ---
1
2
3
4
5
6
11
7
12
8
13
9
14
10
15
--- finish ---
1から5までは普通の関数呼び出しなので連続して出力されていますが、
それ以降はgoroutine1
とgoroutine2
の処理が順不同に行われているのがわかります。
goroutine同士の通信
1つのgoroutineから別のgoroutineへ何かを渡したい場合は、
channel機能を利用することで可能です。
以下にchannelを使用した基本的な同期処理を記述します。
package main
import (
"fmt"
"time"
)
func countUpNumbers(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i // channelへ格納
fmt.Println("countup:", i)
}
}
func catchNumber(ch chan int) {
for i := 0; i < 5; i++ {
num := <-ch // channelから取り出し
fmt.Println("catch: ", num)
}
}
func main() {
fmt.Println("--- start ---")
ch := make(chan int)
go countUpNumbers(ch)
go catchNumber(ch)
time.Sleep(100 * time.Millisecond) // 0.1秒遅延
fmt.Println("--- end ---")
}
--- start ---
catch: 0
countup: 0
countup: 1
catch: 1
catch: 2
countup: 2
countup: 3
catch: 3
catch: 4
countup: 4
--- end ---
catch
とcountup
から出力される数字が同期されている数字であることがわかります。
送信した数字を受信側で受け取るまで送信側は処理を中断するので、このように順番にカウントが増えていきます。
バッファ付きchannelの実装も可能です。
いままで使用していたのはバッファなしチャンネルです。
データをキューに格納し、FIFO形式で扱われます。
実装方法は、ch := make(chan int, [バッファサイズ])
のように指定するだけです。
ベンチ計測
goroutineを使用した並行実行速度を確認してみようと思います。
goはベンチ計測も標準で用意されていて素晴らしいですね。
package bench
import (
"testing"
"time"
)
const COUNT = 100
func printNumbers(begin, end int) {
var numbers []int
for i := begin; i < end; i++ {
numbers = append(numbers, i)
}
}
func BenchmarkPrintNumbers(b *testing.B) {
printNumbers(1, COUNT) // goroutine1
printNumbers(COUNT+1, COUNT*2) // goroutine2
}
func BenchmarkGoPrintNumbers(b *testing.B) {
b.ResetTimer()
go printNumbers(1, COUNT) // goroutine1
go printNumbers(COUNT+1, COUNT*2) // goroutine2
time.Sleep(100 * time.Millisecond) // 0.1ms sleep
}
$ go test -bench . -cpu 1
BenchmarkPrintNumbers 2000000000 0.00 ns/op
BenchmarkGoPrintNumbers 2000000000 0.05 ns/op
PASS
結果を見て見るとgoroutineを使用している方が時間がかかっています。
並行実行で行うタスク自体が軽量すぎると、並行実行にすることのコストの方が上回ってしまいます。
ただこのままではgoroutineの性能が発揮されていないので、少しだけ処理を重く(遅延)を入れてみます。
package bench
import (
"testing"
"time"
)
const COUNT = 100
func printNumbers(begin, end int) {
var numbers []int
for i := begin; i < end; i++ {
time.Sleep(1 * time.Millisecond)
numbers = append(numbers, i)
}
}
func BenchmarkPrintNumbers(b *testing.B) {
printNumbers(1, COUNT) // goroutine1
printNumbers(COUNT+1, COUNT*2) // goroutine2
}
func BenchmarkGoPrintNumbers(b *testing.B) {
b.ResetTimer()
go printNumbers(1, COUNT) // goroutine1
go printNumbers(COUNT+1, COUNT*2) // goroutine2
time.Sleep(100 * time.Millisecond) // 0.1ms sleep
}
$ go test -bench . -cpu 1
BenchmarkPrintNumbers 1000000000 0.27 ns/op 0 B/op 0 allocs/op
BenchmarkGoPrintNumbers 2000000000 0.05 ns/op 0 B/op 0 allocs/op
PASS
0.1秒遅延をいれて見てみると、
普通に関数を呼び出したパターンとgoroutineを使用しているパターンを見比べてみると
5倍ほどの実行時間の差が出ているのがわかります。
まとめ
今回はgoroutineの基本的な使用方法についてまとめてみました。
さらにgoroutineを活用や検証するのは次回、次々回の記事でやろうと思います。