プログラミング言語 Go を読みながらメモ。
第一章 : https://qiita.com/Nabetani/items/077c6b4d3d1ce0a2c3fd
第二章 : https://qiita.com/Nabetani/items/d053304698dfa3601116
第三章 : https://qiita.com/Nabetani/items/2fd9c372fcd8383955a5
第四章 : https://qiita.com/Nabetani/items/59bfd00dc3323883a07f
第五章 : https://qiita.com/Nabetani/items/4b785f1c9b0b26d48475
第六章 : https://qiita.com/Nabetani/items/1c100394a65af6506187
第七章 : https://qiita.com/Nabetani/items/6553ad253af77661e915
で。
ようやく goルーチン。
簡単なパターン
わかりやすいところではこんな感じ。
package main
import "fmt"
func main() {
naturals := make(chan int)
squares := make(chan int)
go func() {
for x := 0; x < 10; x++ {
naturals <- x
}
close(naturals)
}()
go func() {
for {
x, ok := <-naturals
if !ok {
break
}
squares <- x * x
}
close(squares)
}()
for {
x, ok := <-squares
if !ok {
break
}
fmt.Println(x)
}
}
あるいは、名前のある関数にして、引数として chan を受け取るようにすれば
package main
import "fmt"
func counter(out chan<- int) {
for x := 0; x < 100; x++ {
out <- x
}
close(out)
}
func squarer(out chan<- int, in <-chan int) {
for v := range in {
out <- v * v
}
close(out)
}
func printer(in <-chan int) {
fmt.Println("printer")
for v := range in {
fmt.Print(v, " ")
}
}
func main() {
n := make(chan int)
sq := make(chan int)
go counter(n)
go squarer(sq, n)
printer(sq)
}
という感じ。
バッファなし chan を使った場合の実行順
実行順は、以下のソースを実行すると
package main
import "fmt"
func counter(out chan<- int) {
fmt.Print("[C]")
for x := 0; x < 3; x++ {
fmt.Printf("c%d ", x)
out <- x
}
close(out)
}
func squarer(out chan<- int, in <-chan int) {
fmt.Print("[S]")
for v := range in {
fmt.Printf("s%d ", v)
out <- v * v
}
close(out)
}
func printer(in <-chan int) {
fmt.Print("[P]")
for v := range in {
fmt.Printf("p%d ", v)
}
}
func main() {
n := make(chan int)
sq := make(chan int)
go counter(n)
go squarer(sq, n)
printer(sq)
fmt.Println("")
}
出力は
[P][C][S]c0 c1 s0 s1 c2 p0 p1 s2 p4 c3 c4 s3 s4 c5 p9 p16 s5 p25 c6 s6 p36
だったり
[P][C]c0 [S]s0 p0 c1 c2 s1 s2 c3 p1 p4 s3 p9 c4 c5 s4 s5 c6 p16 p25 s6 p36
だったりする。
バッファあり chan を使った場合の実行順
これをバッファ付き
package main
import "fmt"
func counter(out chan<- int) {
fmt.Print("[C]")
for x := 0; x < 7; x++ {
fmt.Printf("c%d ", x)
out <- x
}
close(out)
}
func squarer(out chan<- int, in <-chan int) {
fmt.Print("[S]")
for v := range in {
fmt.Printf("s%d ", v)
out <- v * v
}
close(out)
}
func printer(in <-chan int) {
fmt.Print("[P]")
for v := range in {
fmt.Printf("p%d ", v)
}
}
func main() {
n := make(chan int, 5)
sq := make(chan int, 5)
go counter(n)
go squarer(sq, n)
printer(sq)
fmt.Println("")
}
にすると
[P][C][S]c0 c1 c2 s0 s1 s2 c3 c4 s3 s4 p0 c5 p1 p4 p9 p16 c6 s5 s6 p25 p36
だったり
[P][C]c0 c1 c2 c3 c4 c5 [S]s0 s1 s2 s3 s4 s5 c6 s6 p0 p1 p4 p9 p16 p25 p36
だったりする。
なるほどバッファが使われている感じ。
もうちょっとあからさまな例を。
package main
import (
"fmt"
"os"
"strconv"
"time"
)
func counter(out chan<- int) {
fmt.Print("[C]")
for x := 0; x < 10; x++ {
fmt.Printf("c%d ", x)
out <- x
}
close(out)
}
func eater(in <-chan int) {
fmt.Print("[E]")
for v := range in {
fmt.Printf("e%d ", v)
time.Sleep(10)
}
}
func main() {
b, _ := strconv.Atoi(os.Args[1])
n := make(chan int, b)
go counter(n)
eater(n)
fmt.Println("")
}
での出力は、下表の通り:
コマンドライン引数 | 出力例(実際毎回変わるけど) |
---|---|
0 |
[E][C]c0 c1 e0 e1 c2 c3 e2 e3 c4 c5 e4 e5 c6 c7 e6 e7 c8 c9 e8 e9 |
5 |
[E][C]c0 c1 c2 c3 c4 c5 c6 e0 e1 e2 e3 c7 c8 c9 e4 e5 e6 e7 e8 e9 |
100 |
[E][C]c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 e0 e1 e2 e3 e4 e5 e6 e7 e8 e9 |
なるほどバッファがある感じ。
goルーチンのリーク?
別の例では:
package main
import (
"fmt"
"os"
"strconv"
"time"
)
func get(n int) string {
time.Sleep(time.Duration(n) * time.Millisecond)
fmt.Printf("<%d>", n)
return fmt.Sprintf("[%d]", n)
}
func query() string {
b, _ := strconv.Atoi(os.Args[1])
s := make(chan string, b)
for _, i := range []int{50, 60, 20, 70, 30, 10, 90, 80, 40} {
ii := i
go func() { s <- get(ii) }()
}
//defer close(s) 必要なのかどうかわからない...
return <-s
}
func main() {
fmt.Println(query())
}
たぶん、この例で引数を 0 にすると goルーチンがリークするという趣旨のことが p269 に書いてあるんだけど、よくわからなかった。
並列実行
並列実行は、以下のようなパターンで:
package main
import (
"fmt"
"time"
)
func main() {
numbers := []int{1, 10, 2, 20}
ch := make(chan struct{})
for _, nn := range numbers {
n := nn
go func(x int) {
time.Sleep(time.Duration(x) * time.Millisecond) // 重い処理のつもり
fmt.Print(x, " ")
ch <- struct{}{}
}(n)
}
for range numbers {
<-ch
}
}
goルーチンだけだと、終了を待ち合わせられない。待ち合わせるためにデータのやり取りのない chan を使う。
sync.WaitGroup の利用
sync.WaitGroup
というものがあって、便利そうな感じ。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
ch := make(chan string, 3)
var wg sync.WaitGroup
for _, msg := range []string{"foo", "bar", "baz", "hoge", "fuga", "piyo"} {
wg.Add(1)
go func(m string) {
defer wg.Done()
time.Sleep(10 * time.Millisecond)
fmt.Println(m)
}(msg)
}
wg.Wait() // これがないと何も出ない
close(ch)
}
wg.Wait()
のところが便利。
並列性の制限
並列にしすぎると大抵不幸になるので、制限したい。
chan によるトークン
package main
import (
"fmt"
"sync"
"time"
)
func main() {
ch := make(chan string)
tokens := make(chan struct{}, 3)
var wg sync.WaitGroup
for msg := 0; msg < 10; msg++ {
wg.Add(1)
go func(m int) {
tokens <- struct{}{} // トークン獲得
defer wg.Done()
fmt.Printf("%d started\n", m)
time.Sleep(10 * time.Millisecond)
fmt.Printf("%d ended\n", m)
<-tokens // トークン解放
}(msg)
}
wg.Wait() // これがないと何も出ない
close(ch)
}
これを実行すると
9 started
1 started
0 started
9 ended
0 ended
7 started
6 started
1 ended
8 started
8 ended
3 started
6 ended
7 ended
4 started
2 started
2 ended
5 started
3 ended
4 ended
5 ended
のように、ちゃんと 3個に制限される。
goルーチンの数による制限
chan のバッファを利用せずとも制限できる:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
ch := make(chan int)
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
go func() {
for m := range ch {
defer wg.Done()
fmt.Printf("%d started\n", m)
time.Sleep(10 * time.Millisecond)
fmt.Printf("%d ended\n", m)
}
}()
}
for i := 0; i < 10; i++ {
ch <- i
}
wg.Wait()
close(ch)
}
Select
ポイントは、受け入れ可能なケースが複数あると乱数が使われるっていう点かな。
フィボナッチ
練習がてら、無駄に遅いフィボナッチを書いてみた:
package main
import "fmt"
func fibo(ch chan int64, x int64) {
if x <= 1 {
ch <- x
return
}
prev1 := make(chan int64)
defer close(prev1)
go fibo(prev1, x-1)
prev2 := make(chan int64)
defer close(prev2)
go fibo(prev2, x-2)
ch <- <-prev1 + <-prev2
}
func main() {
var i int64
const N = 32
var chs [N]chan int64
for i = 0; i < N; i++ {
chs[i] = make(chan int64)
}
for i = 0; i < N; i++ {
go fibo(chs[i], i)
}
for i = 0; i < N; i++ {
fmt.Printf("fibo(%v) = %v\n", i, <-chs[i])
}
}
実際のところ、 goルーチンを使わないバージョンと比べて 10倍以上遅い。
そういうものか。
最後に
go ルーチンと chan のことはまだあんまりわかっていない。
もうちょっと使ってみないとメンタルモデルがしっかり構築されないんだろうなと思う。