いっきのblog

技術とか色々

GoroutineとChannelについて学ぶ

前回Goのインタフェースについて書いた。

kzkohashi.hatenablog.com (アイキャッチが更新されてない・・)

今回はGoの真髄とも言える、ゴルーチンとチャネルによる並行処理(Concurrent)について学んでいく。

並行(Concurrent)処理と並列(Parallel)処理

間違えやすいのだが、並行処理は並列処理と概念的に異なる。

こちらの記事がわかりやすかったので、引用させていただく。

Concurrent(並行)は「複数の動作が、論理的に、順不同もしくは同時に起こりうる」こと Parallel(並列)は、「複数の動作が、物理的に、同時に起こること」
引用: freak-da.hatenablog.com

並行は1人の人が複数の仕事をし、並列は複数の人が複数の仕事をしている状態だ。
とはいっても、並行処理は擬似並列ともいえるので、日本語的には「並列処理」を使っても問題ないと思う。

この辺りも面白かった。

興味がある人は以下を読むと良さそう。
自分は近々届くので、また別途まとめたい。

www.oreilly.co.jp

Goroutineで並行処理を行う。

Goroutineの実装は簡単で、関数の前にgoをつけるだけだ。
これだけで並行処理がとりあえずは実装できる。

func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}


func main() {
    go say("world")
    say("hello")
}

引用: https://go-tour-jp.appspot.com/concurrency/1

並行処理のプログラミングをする際は、mainでの処理とは別に処理が走ってしまうので、Channelをうまく使って制御したり、Sleepで待つという方法もある。
Sleepは処理が終わる終わらないに限らず、その時間しかまたないのでよくない。

Channelを使う

A Tour Goをやってもあまり理解しなかったが、以下の記事がよかったのでこちらを見た方がいい。
channelは値をやり取りをするためのキューみたいなもんだ。しかも、goroutine(スレッド)間で値の受け渡しを安全にできるので、とても簡単に扱える。

qiita.com

func main() {
  // 容量2のchannel型で作成
  c := make(chan string, 2)
 // データを入れる
  c <- "hello"
  // データを入れる
  c <- "hello2"
  // c <- "hello3" 容量2のため3個目をいれたらエラー
  // 最初に入れたデータをchannelから取り出す
  fmt.Println(<-c)
  fmt.Println(<-c)
  // fmt.Println(<-c) 3つ目はない

  c = make(chan string, 1)
  c <- "world"
  fmt.Println(<-c)
}

Exercise: Web Crawler を解く

A Tour Goの最後に、goroutineの問題があったので解いて見た。
go-tour-jp.appspot.com

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, resultUrl ResultUrl) (body string, urls []string, err error)
}

type ResultUrl struct {
    v   map[string]int
}

// Value returns the current value of the counter for the given key.
func checkUrl(c *ResultUrl, key string) bool {
    return c.v[key] == 1
}


// Crawl uses fetcher to recursively crawl
// pages starting with url, to a maximum of depth.
func Crawl(url string, depth int, fetcher Fetcher, c ResultUrl, quit chan bool) {
    // TODO: Fetch URLs in parallel.
    // TODO: Don't fetch the same URL twice.
    // This implementation doesn't do either:
    if depth <= 0 {
        quit <- true
        return
    }

    if checkUrl(&c, url) {
        quit <- true
        return
    }

    body, urls, err := fetcher.Fetch(url, c)
    if err != nil {
        fmt.Println(err)
        quit <- true
        return
    }
    fmt.Printf("found: %s %q\n", url, body)

    quit2 := make(chan bool)
    for _, u := range urls {
        go Crawl(u, depth-1, fetcher, c, quit2)
        <-quit2
    }

    quit <- true
    return
}

func main() {
    c := ResultUrl{v: make(map[string]int)}
    quit := make(chan bool)
    go Crawl("https://golang.org/", 4, fetcher, c, quit)
    <-quit
}

// 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, c ResultUrl) (string, []string, error) {
    c.v[url] = 1

    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/",
        },
    },
}

すでにクロールしたURLを判定する処理に、sync.Mutexを最初入れてたが、入れなくてもできたのでちょっと怪しい。
キモとしては、channel型のquitに値を入れ、<-quitgoroutineの処理を待つことができる。

終わりに

Goの勉強を駆け足でやってみた。
最初は意味がわからなかったが、毎日少しずつ見ていくと慣れていくもんだな。
次からはGoAPIサーバー作ることをしようかな。