今からgoroutineにまつわる簡単なクイズを出します。間違えたら「いいね」していってください。
packageとimportは適当に設定されている体で。
case0
func main() {
for i := 0; i < 5; i++ {
go fmt.Println(i)
}
}
- 0から4まで順番に出力する
- 0から4をバラバラに出力する
- 何も出力せず正常終了する
- パニックする
- その他
解答(折りたたみ)
3
mainのgoroutineは、立ち上げた5つのgoroutineがprintするのを待たずに終了し、その結果すべてのgoroutineが処理前に落ちるので何も出力されません。
こんな初歩的なところで躓いたあなたにはこの記事は参考になるはずなので、いいねを押して次の問題へ。
case1
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
go fmt.Println(i)
wg.Done()
}
wg.Wait()
}
上記を踏まえて修正しました。このコードはどうなる?
- 0から4まで順番に出力する
- 0から4をバラバラに出力す
- 何も出力せず正常終了する
- ぬるぽでパニックする
- その他の理由でパニックする
解答(折りたたみ)
5
waitGroupの使い方を誤っているので、negative WatiGroup counterでパニックします。
わからない方は……
つ公式ドキュメント
「ガッ」
と言われて懐かしくなったあなたはいいねを押して次へ
case2
sync.WaitGroupのドキュメントは読みましたね。じゃあこれは?
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
wg.Add(1)
go fmt.Println(i)
wg.Done()
}
wg.Wait()
}
- 0から4まで順番に出力する
- 0から4をバラバラに出力する
- 何も出力せず正常終了する
- negative WaitGroup counterでパニックする
- その他
解答(折りたたみ)
3
またもやsync.WaitGroupの使い方が間違っているので、今回は出力を待たずに終了します。
wg.Done()はmainではなく、新しく立てたgoroutineの中で使用しましょう。
公式ドキュメントを真面目に読まずに次の問題に進んで間違えたあなたはいいねを押して公式ドキュメントを読みましょう。
case3
いい加減何かを出力したくなってきましたので今回は、エラーを履かずにgoroutineの処理を待って何かを標準出力するコードを書いてみました。
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
fmt.Println(i)
wg.Done()
}()
}
wg.Wait()
}
- 0から4まで順番に出力する
- 0から4をバラバラに出力する
- 何も出力せず正常終了する
- 4を5回出力する
- その他の何かを出力する
4の選択肢があやしすぎるって? 今回は答えを簡単にしようと思っただけです。
解答(折りたたみ)
5. (5を5回出力する)
どうしてこの答えになるかわかりますか? (わからなければいいねを……)
goroutine内のiは、for文のスコープのiを参照します。つまり、関数が開始された時点ではなく、print実行時点でのiを参照します。for文の実行がprintするより先に終わるのは先述なので、問題になるのがiが最終的に取る値です。
これを踏まえると4の答えを選びそうになりますが、この問題の肝はgoroutineだけでなく、for文の処理順にあります。for文は、左から、初期化処理、ループ開始時の判定、ループ終了時の処理となっています。ループ終了時の処理であるi++
は5回のループが終了するまでに、5回実行されます。ゆえに、for文終了時の値は5。
引っ掛け問題に騙されて4を選んだあなたは、いいねを押してからコードを試してみてください。
case3 補足
この問題を同僚と解いていたら
「for文のスコープって何? main関数のスコープじゃないの?」
と言われたので、
func main() {
i := -1
for i := 0; i < 1; i++ {
fmt.Println(i)
i := 1
fmt.Println(i)
}
fmt.Println(i)
}
出力は?
解答(折りたたみ)
0
1
-1
毎回代入しているので毎回違う出力が得られます。
スコープも違うよ!
勘違いしてたら感心していいね押すように。
case4
やっとやりたかったことにたどり着きました。長かった。
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
wg.Add(1)
go func(i int) {
fmt.Println(i)
wg.Done()
}(i)
}
wg.Wait()
}
- 0から4まで順番に出力する
- 0から4をバラバラに出力する
- 何も出力せず正常終了する
- 5を5回出力する
- その他の挙動をする
解答(折りたたみ)
2
多分最初のコードがやりたかったことはこれです。(適当)
case5
もうちょっとだけ続きます。
これが並列処理なのは承知の上で、それでも0から4を順番に表示したいとします。そこで、sync.Mutexを使うわけですが、sync.Mutexご存知ですか?
func main() {
wg := sync.WaitGroup{}
mu := sync.Mutex{}
for i := 0; i < 5; i++ {
wg.Add(1)
go func(i int) {
mu.Lock()
fmt.Println(i)
mu.Unlock()
wg.Done()
}(i)
}
wg.Wait()
}
sync.Mutexの使い方を学んだ上で上記のコードの挙動を答えてください。
- 0から4まで順番に出力する
- 0から4をバラバラに出力する
- 何も出力せず正常終了する
- 5を5回出力する
- その他の挙動をする
解答(折りたたみ)
2
さっきまでと同じようにgoroutineの中でLockを呼び出している以上順番は保証されません。
最終問題 Case6
速いのはどっち?
さて、並列処理についてなんとなくわかったところで、今まで学んだこととは全く関係ない問題を出します。
ロギングを1024回直列と並列で行うのでどちらが速いか当ててください。
直列
func serial() {
for i := 0; i < 1024; i++ {
log.Println("はろーわーるど")
}
}
並列
func parallel() {
wg := sync.WaitGroup{}
for i := 0; i < 1024; i++ {
wg.Add(1)
go func() {
log.Println("はろーわーるど")
wg.Done()
}()
}
wg.Wait()
}
選択肢
- 1000並列もやっているんだから並列のほうが100倍以上速い
- 100倍以上とは行かないけど顕著に並列のほうが速い
- 直列と並列で同程度の速さ。(試すたびに勝ち負けが変わるくらい)
- 並列の強みを活かせていないので直列のほうがそれなりに速い
- 引っ掛けを見ぬいたので、100倍以上直列が速いことは確定的に明らか
解答(折りたたみ)
4
間違えたらとりあえずいいね押しとけ、な?
冗談はさておき、今回の場合は直列の方が速いです
なぜなら、log.Printlnはロックをかけるからです。内部でsync.Mutexを持ち、ロックをかけるので、ブロックします。詳しくはブロッキングIOとかでググると良いよ。
つまり、1024回のlog.Println()は、直列にしか実行できません。それ故、余計な要素の多い、並列処理のほうが遅くなります。
一応、wgを触る部分を別関数として作り、ベンチマークを取ると
func wg() {
wg := sync.WaitGroup{}
for i := 0; i < 1024; i++ {
wg.Add(1)
go wg.Done()
}
wg.Wait()
}
$ go test --bench . --benchmem --benchtime 5s 2>/dev/null
goos: linux
goarch: amd64
pkg: github.com/aimof/try/goroutine
BenchmarkSerial-4 10000 619974 ns/op 32773 B/op 1024 allocs/op
BenchmarkParallel-4 10000 879264 ns/op 33303 B/op 1030 allocs/op
BenchmarkWg-4 50000 179671 ns/op 16 B/op 1 allocs/op
PASS
ok github.com/aimof/try/goroutine 25.962s
並列の実行時間=直列の実行時間+WaitGroupの実行時間+80マイクロ秒です。だいたいこんなもんですかね。
IOのブロックについて知らなかったことはとりあえずいいねをおしてここまで!
クイズは終わりです。クイズが楽しかった方、全問正解した方は記念にいいねを押していってください。
以上「鬱陶しいくらい「いいね」するよう勧めてくるgoroutineクイズ」でした。
鬱陶しいくらい「いいね」をすすめるという酔いに任せた謎コンセプトでしたが、クイズ自体は酔った頭で真面目に作りました。楽しんでいただけたら幸いです。
追記
ネタ記事にいいねくださった皆様ありがとうございます。クイズが少なくて申し訳ないので、追加しておきます。(いいね)
仕事の昼休みに書いているのですが、同じく仕事中に見ている人はいいねをするように。
Case7
func main() {
for i := 0; i < 10000000; i++ {
c := make(chan bool)
go goroutine(c)
defer close(c)
}
}
func goroutine(c chan bool) {
select {
case <-c:
return
}
}
シンプルかつものすごく問題のあるコードがここにあります。このコードは普通のコンピュータでは正常に実行できません。その理由をズバリ答えてください。危険なので実行はしないほうがいいですよ。
解答(折りたたみ)
goroutineがリークし、メモリが足りなくなるから
このgoroutineは終わらないため、main関数の終了までgoroutineとチャネルは生き残ります。結果メモリが足りなくなります。
ちなみに「deferの使い方を間違えており、チャネルがリークするから」と答えた方は反省して、いいねしてください。
func main() {
for i := 0; i < 10000000; i++ {
c := make(chan bool)
defer close(c)
}
}
この程度のメモリ使用なら最近のコンピュータは普通に耐えます。
Case8
func main() {
c := make(chan bool)
for i := 0; i < 10000000; i++ {
c <- true
c := make(chan bool)
go goroutine(c)
defer close(c)
}
}
func goroutine(c chan bool) {
select {
case <-c:
return
}
}
ちょっと修正しました。
まだバグっていることに気づいた方は「いいね」
このコードを実行するとどうなる?
- やっぱりメモリが食いつぶされる
- 今度はパニックする
- 二重定義でコンパイルを通らない
- goroutineが立ち上がっていないのにチャネルを使うのでdead lockで止まる
- その他
解答(折りたたみ)
コンパイルは通ります。通りますが、今回は
fatal error: all goroutines are asleep - deadlock!
こうなります。goroutineが立ち上がってない状態でチャネルを使ってはいけないのです。
ちなみにデッドロックはパニックではありません。