A Tour of Goをやるぞ(Concurrency)

これをやる

A Tour of Go

Goroutines

go f(x)

で別スレッド関数を実行できる。

Goroutinesは同じアドレス空間で動くので、共有メモリへのアクセス時は同期する必要がある。

package main

import (
    "fmt"
    "time"
)

type Test struct {
    value int
}

func (test *Test) add(v int) {
    test.value = test.value + v
}

func add100(test *Test) {
    for i := 0; i < 100; i++ {
        test.add(1)
    }
}

func main() {
    v := Test{0}
    go add100(&v)
    go add100(&v)
    go add100(&v)
    time.Sleep(10 * time.Millisecond)

    fmt.Println(v)
}

これおかしなことになるかと思ったけどなんか毎回300で出力される。

Channels

package main

import "fmt"

func test(c chan int) {
    c <- 1
}

func main() {
    c := make(chan int)
    go test(c)
    x := <-c // ここで同期する

    fmt.Println(x)
}
  • 別のスレッドから値を受け取る
  • 受信側が同期する

ための機能という理解でいいのだろうか。

package main

import "fmt"
import "time"

func test(c chan int) {
    c <- 1
    time.Sleep(10000)
    fmt.Println("wow")
}

func main() {
    c := make(chan int)
    go test(c)
    x := <-c // ここで同期する

    fmt.Println(x)
}

こうすると"wow"が出力されないのはなんでだろう。 同期するのはchannelsの受信についてだけで関数については同期せず、main関数が終わるとスレッドごと破棄されるのかな。

package main

import "fmt"
import "time"

func test(c chan int) {
    time.Sleep(10000)
    fmt.Println("wow")
    c <- 1
}

func main() {
    c := make(chan int)
    go test(c)
    x := <-c // ここで同期する   
    fmt.Println(x)
}

これは"wow"が出力されるので前者は正しそう。

Channelsのバッファ

Channelsはバッファをもてる。Channelsにバッファよりも多く値を送信するとエラー。

package main

import "fmt"

func test(c chan int) {
    c <- 1
}

func main() {
    ch := make(chan int, 1)
    go test(ch)
    go test(ch)
    go test(ch)
    x, y, z := <-ch, <-ch, <-ch
    fmt.Println(x, y, z)
}

なぜこれはエラーにならないのだろう。

package main

import "fmt"

func main() {
    ch := make(chan int, 1)
    ch <- 1
    ch <- 1
    ch <- 1
    x, y, z := <-ch, <-ch, <-ch
    fmt.Println(x, y, z)
}

これはエラーになる。関数呼び出しの時にチャネルがコピーされて別の値になる的な?

package main

import "fmt"

func test(c *chan int) {
    *c <- 1
}

func main() {
    ch := make(chan int, 1)
    go test(&ch)
    go test(&ch)
    go test(&ch)
    x, y, z := <-ch, <-ch, <-ch
    fmt.Println(x, y, z)
}

ポインタは別になるようだがこれでもやはりエラーにならない。

package main

import "fmt"

func test(c chan int) {
    for i := 0; i < 3; i++ {
        c <- 1
    }
    close(c)
}

func test2(c chan int){
    v, ok := <-c
    fmt.Println(v,ok)
}

func main() {
    ch := make(chan int, 4)
    go test(ch)
    test2(ch)
    test2(ch)
    test2(ch)
    test2(ch) // 0 false
}

channelsに値は3回しか送られてこないが、closeによって待機を打ち切れる。

package main

import "fmt"

func test(c chan int) {
    for i := 0; i < 3; i++ {
        c <- 1
    }
    close(c)
}

func main() {
    ch := make(chan int, 4)
    go test(ch)
    for i := range ch {
        fmt.Println(i)
    }
}

rangeでchannelsが閉じるまで値を受けられる。

Select

受信時のSelect

package main

import (
    "fmt"
    "time"
)

func test(c1, c2 chan int) {
    c1 <- 1
    time.Sleep(1000)
    fmt.Println("send 1")
    c2 <- 2
    fmt.Println("send 2")
}

func main() {

    c1 := make(chan int)
    c2 := make(chan int)

    go test(c1, c2)

    for {
        select {
        case x := <-c1: // c1が送信されたら受信しつつ実行
            fmt.Println(x)
        case y := <-c2: // c2が送信されたら受信しつつ実行
            fmt.Println(y) 
            return
        }
    }
}

複数のチャネルを同期できる。

送信時のSelect

package main

import (
    "fmt"
    "time"
)

func test(c1, c2 chan int) {
    for {
        select {
        case c1<-1: // c1を受信されたら送信しつつ実行
            fmt.Println("receive 1")
        case c2<-2: // c2を受信されたら送信しつつ実行
            fmt.Println("receive 2")
            return
        }
    }

}

func main() {

    c1 := make(chan int)
    c2 := make(chan int)

    go test(c1, c2)
 
    fmt.Println(<-c1)
    fmt.Println(<-c2)
 
    time.Sleep(1000)
}

Defaultによる待機

package main

import (
    "fmt"
    "time"
)

func test(c1, c2 chan int) {
    c1 <- 1
    time.Sleep(1000)
    fmt.Println("send 1")
    c2 <- 2
    fmt.Println("send 2")
}

func main() {

    c1 := make(chan int)
    c2 := make(chan int)

    go test(c1, c2)

    for {
        select {
        case x := <-c1: // c1が送信されたら受信しつつ実行
            fmt.Println(x)
        case y := <-c2: // c2が送信されたら受信しつつ実行
            fmt.Println(y)
            return
        default:
            fmt.Println("not received") // 待機中に実行される
        }
    }
}

Exercise: Equivalent Binary Trees

package main

import "golang.org/x/tour/tree"
import "fmt"

// Walk walks the tree t sending all values
// from the tree to the channel ch.
func Walk(t *tree.Tree, ch chan int) {
    waking(t, ch)
    close(ch)
}

func waking(t *tree.Tree, ch chan int) {
    if t.Left != nil {
        waking(t.Left, ch)
    }

    ch <- t.Value

    if t.Right != nil {
        waking(t.Right, ch)
    }
}

// Same determines whether the trees
// t1 and t2 contain the same values.
func Same(t1, t2 *tree.Tree) bool {
    ch1 := make(chan int)
    ch2 := make(chan int)
    go Walk(t1, ch1)
    go Walk(t2, ch2)

    for value1 := range ch1 {
        value2 := <-ch2
        if value1 != value2 {
            return false
        }
    }

    _, ok := <-ch2
    if ok {
        return false
    }

    return true
}

func main() {
    ch := make(chan int)

    // Walk test
    go Walk(tree.New(1), ch)
    for v := range ch {
        fmt.Println(v)
    }

    // Same test
    fmt.Println(Same(tree.New(1), tree.New(1)))
    fmt.Println(Same(tree.New(1), tree.New(2)))
    fmt.Println(Same(tree.New(2), tree.New(1)))

}

A Tour of Go. Exercise: Web Crawler

URLのプールがあって非同期でそっからランダムにフェッチ、みたいな路線で考えてたんだけどよくわかんなくなった。

で、これを半日眺めて自分でも書いて見たらまるっきり同じになってしまった。 A Tour of Go. Exercise: Web Crawler · GitHub

外側のループがfetchの回数とチャネルで結果を受け取る回数を同期させるためのものだと気づくのにやたら時間がかかった。

package main

import (
    "fmt"
)

type Fetcher interface {
    // Fetch returns the body of URL and
    // a slice of URLs found on that page.
    Fetch(url string) (body string, urls []string, err error)
}

type Result struct {
    depth int
    urls  []string
    url   string
    body  string
    err   error
}

// Crawl uses fetcher to recursively crawl
// pages starting with url, to a maximum of depth.
func Crawl(url string, depth int, fetcher Fetcher) {

    ch := make(chan Result, 1)

    crawledUrls := make(map[string]bool)
    crawledUrls[url] = true // 重複回避用
    go fetch(url, 0, ch)

    // fetchした回数と同じだけループ
    for i := 1; i > 0; i-- {
        r := <-ch // なんらかのスレッドがfetchに完了するのを待機
        if r.err != nil {
            fmt.Println(r.err)
            continue
        }

        fmt.Printf("found: %s %q\n", r.url, r.body)

        if r.depth >= depth { // 最大のdepthまで行ったら次はクロールしない
            continue
        }

        for _, u := range r.urls {
            if !crawledUrls[u] {
                crawledUrls[u] = true // ここは同期的に動いてるのでmapは壊れない
                go fetch(u, r.depth+1, ch)
                i++ // fetchする回数をカウントし、ループとfetchの回数を合わせる
            }
        }
    }
    return
}

func fetch(url string, depth int, ch chan Result) {
    body, urls, err := fetcher.Fetch(url)
    ch <- Result{depth, urls, url, body, err}
}

func main() {
    Crawl("https://golang.org/", 4, fetcher)
}

// fakeFetcher is Fetcher that returns canned results.
type fakeFetcher map[string]*fakeResult

type fakeResult struct {
    body string
    urls []string
}

func (f fakeFetcher) Fetch(url string) (string, []string, error) {
    if res, ok := f[url]; ok {
        return res.body, res.urls, nil
    }
    return "", nil, fmt.Errorf("not found: %s", url)
}

// fetcher is a populated fakeFetcher.
var fetcher = fakeFetcher{
    "https://golang.org/": &fakeResult{
        "The Go Programming Language",
        []string{
            "https://golang.org/pkg/",
            "https://golang.org/cmd/",
        },
    },
    "https://golang.org/pkg/": &fakeResult{
        "Packages",
        []string{
            "https://golang.org/",
            "https://golang.org/cmd/",
            "https://golang.org/pkg/fmt/",
            "https://golang.org/pkg/os/",
        },
    },
    "https://golang.org/pkg/fmt/": &fakeResult{
        "Package fmt",
        []string{
            "https://golang.org/",
            "https://golang.org/pkg/",
        },
    },
    "https://golang.org/pkg/os/": &fakeResult{
        "Package os",
        []string{
            "https://golang.org/",
            "https://golang.org/pkg/",
        },
    },
}

他の人の回答もいくつか見てみようと思うがとりあえず一旦A Tour of Goは終わりにする。 なんか作ろうとも思ったけど

  • それ以前のGo力が足りなさすぎ
  • アルゴリズム考えたり非同期とか再帰とか苦手すぎ
  • これ読みたいけど結構他の本積んでるんだよな

『Goならわかるシステムプログラミング』 – 技術書出版と販売のラムダノート

ということで他に読んでない本をとりあえず消化しつつGoについては Go: The Complete Developer's Guide (Golang) | Udemy を一通り見て基礎的なところをある程度体系的にこなしていこうかなと思っている次第。

アルゴリズム考えたり非同期とか再帰とか苦手すぎ

これはudemy見終わったらなんかあるあるなアルゴリズムを自分で実装してみる会をやろうかな。