Help us understand the problem. What is going on with this article?

プログラミング言語Goを読みながらメモ(第八章)

More than 1 year has passed since last update.

プログラミング言語 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ルーチン。

簡単なパターン

わかりやすいところではこんな感じ。

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 を受け取るようにすれば

go
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 を使った場合の実行順

実行順は、以下のソースを実行すると

go
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 を使った場合の実行順

これをバッファ付き

go
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
だったりする。

なるほどバッファが使われている感じ。

もうちょっとあからさまな例を。

go
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ルーチンのリーク?

別の例では:

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 に書いてあるんだけど、よくわからなかった。

並列実行

並列実行は、以下のようなパターンで:

go
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 というものがあって、便利そうな感じ。

go
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 によるトークン

go
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 のバッファを利用せずとも制限できる:

go
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

ポイントは、受け入れ可能なケースが複数あると乱数が使われるっていう点かな。

フィボナッチ

練習がてら、無駄に遅いフィボナッチを書いてみた:

go
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 のことはまだあんまりわかっていない。
もうちょっと使ってみないとメンタルモデルがしっかり構築されないんだろうなと思う。

Nabetani
横浜へなちょこプログラミング勉強会をやっていました。 / CodeIQ の出題者でした。 / 日経 WinPC に連載を持っていました(名義が違うけど) / Yokohama rb に半分ぐらい参加しています。 / twitter : http://twitter.com/Nabetani
https://nabetani.hatenadiary.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした