並行処理

The Go Authors

目次

1. ゴルーチン

goroutine (ゴルーチン)は、Goのランタイムに管理される軽量なスレッドです。

go f(x, y, z)

と書けば、新しいgoroutineが実行されます。

f(x, y, z)

f , x , y , z の評価は、実行元(current)のgoroutineで実行され、 f の実行は、新しいgoroutineで実行されます。

goroutineは、同じアドレス空間で実行されるため、共有メモリへのアクセスは必ず同期する必要があります。 sync パッケージは同期する際に役に立つ方法を提供していますが、別の方法があるためそれほど必要ありません。 (次のスライドで説明します)

package main

import (
    "fmt"
    "time"
)

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")
}

2. チャネル

チャネル( Channel )型は、チャネルオペレータの <- を用いて値の送受信ができる通り道です。

ch <- v    // v をチャネル ch へ送信する
v := <-ch  // ch から受信した変数を v へ割り当てる

(データは、矢印の方向に流れます)

マップとスライスのように、チャネルは使う前に以下のように生成します:

ch := make(chan int)

通常、片方が準備できるまで送受信はブロックされます。これにより、明確なロックや条件変数がなくても、goroutineの同期を可能にします。

サンプルコードは、スライス内の数値を合算し、2つのgoroutine間で作業を分配します。 両方のgoroutineで計算が完了すると、最終結果が計算されます。

package main

import "fmt"

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    c <- sum // send sum to c
}

func main() {
    s := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // receive from c

    fmt.Println(x, y, x+y)
}

3. バッファ化されたチャネル

チャネルは、 バッファ ( buffer )として使えます。 バッファを持つチャネルを初期化するには、 make の2つ目の引数にバッファの長さを与えます:

ch := make(chan int, 100)

バッファが詰まった時は、チャネルへの送信をブロックします。 バッファが空の時には、チャネルの受信をブロックします。

バッファが詰まるようにサンプルコードを変更し、何が起きるのかを見てみてください。

package main

import "fmt"

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

4. rangeとclose

送り手は、これ以上の送信する値がないことを示すため、チャネルを close できます。 受け手は、受信の式に2つ目のパラメータを割り当てることで、そのチャネルがcloseされているかどうかを確認できます:

v, ok := <-ch

受信する値がない、かつ、チャネルが閉じているなら、 ok の変数は、 false になります。

ループの for i := range c は、チャネルが閉じられるまで、チャネルから値を繰り返し受信し続けます。

注意: 送り手のチャネルだけをcloseしてください。受け手はcloseしてはいけません。 もしcloseしたチャネルへ送信すると、パニック( panic )します。

もう一つ注意: チャネルは、ファイルとは異なり、通常は、closeする必要はありません。 closeするのは、これ以上値が来ないことを受け手が知る必要があるときにだけです。 例えば、 range ループを終了するという場合です。

package main

import (
    "fmt"
)

func fibonacci(n int, c chan int) {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        c <- x
        x, y = y, x+y
    }
    close(c)
}

func main() {
    c := make(chan int, 10)
    go fibonacci(cap(c), c)
    for i := range c {
        fmt.Println(i)
    }
}

5. select

select ステートメントは、goroutineを複数の通信操作で待たせます。

select は、複数ある case のいずれかが準備できるようになるまでブロックし、準備ができた case を実行します。 もし、複数の case の準備ができている場合、 case はランダムに選択されます。

package main

import "fmt"

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

6. Default Selection

どの case も準備ができていないのであれば、 select の中の default が実行されます。

ブロックせずに送受信するなら、 defaultcase を使ってください:

select {
case i := <-c:
    // use i
default:
    // receiving from c would block
}
package main

import (
    "fmt"
    "time"
)

func main() {
    tick := time.Tick(100 * time.Millisecond)
    boom := time.After(500 * time.Millisecond)
    for {
        select {
        case <-tick:
            fmt.Println("tick.")
        case <-boom:
            fmt.Println("BOOM!")
            return
        default:
            fmt.Println("    .")
            time.Sleep(50 * time.Millisecond)
        }
    }
}

7. 練習:等価な二分木

葉に同じ順序の値を保持する、異なる多くの二分木( binary tree )]があります。 例えば、ここに "1, 1, 2, 3, 5, 8, 13" を保持する2つの二分木があります。

2つの二分木が同じ順序を保持しているかどうかを確認する機能は、多くの言語においてかなり複雑です。 シンプルな解決方法を記述するために、Goの並行性( concurrency )とチャネルを利用してみます。

例では、型を以下のように定義している tree パッケージを利用します:

type Tree struct {
    Left  *Tree
    Value int
    Right *Tree
}

続く...

8. 練習:等価な二分木(続)

1. Walk 関数を実装してください。

2. Walk 関数をテストしてください。

関数 tree.New(k) は、値( k, 2k, 3k, ..., 10k )をもつ、ランダムに構造化 (しかし常にソートされています) した二分木を生成します。

新しいチャネル ch を生成し、 Walk を動かしましょう:

go Walk(tree.New(1), ch)

そして、そのチャネルから値を読み出し、10個の値を表示してください。 それは、 1, 2, 3, ..., 10 という表示になるでしょう。

3. Same 関数を実装してください。 t1t2 が同じ値を保存しているどうかを判断するため、 Walk を使ってください。

4. Same 関数をテストしてください。

Same(tree.New(1), tree.New(1)) は、 true を返すように、 Same(tree.New(1), tree.New(2)) は、 false を返すようにします。

Tree のドキュメントは こちら です。

package main

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

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

// Same determines whether the trees
// t1 and t2 contain the same values.
func Same(t1, t2 *tree.Tree) bool

func main() {
}

9. sync.Mutex

チャネルが、goroutine間で通信するための素晴らしい方法であることを見てきました。

しかし、通信が必要ない場合はどうでしょう? コンフリクトを避けるために、一度に1つのgoroutineだけが変数にアクセスできるようにしたい場合はどうでしょうか?

このコンセプトは、排他制御( mutual exclusion )と呼ばれ、このデータ構造を指す一般的な名前は mutex (ミューテックス)です。

Goの標準ライブラリは、排他制御をsync.Mutexと次の二つのメソッドで提供します:

Inc メソッドにあるように、 LockUnlock で囲むことで排他制御で実行するコードを定義できます。

Value メソッドのように、mutexがUnlockされることを保証するために defer を使うこともできます。

package main

import (
    "fmt"
    "sync"
    "time"
)

// SafeCounter is safe to use concurrently.
type SafeCounter struct {
    v   map[string]int
    mux sync.Mutex
}

// Inc increments the counter for the given key.
func (c *SafeCounter) Inc(key string) {
    c.mux.Lock()
    // Lock so only one goroutine at a time can access the map c.v.
    c.v[key]++
    c.mux.Unlock()
}

// Value returns the current value of the counter for the given key.
func (c *SafeCounter) Value(key string) int {
    c.mux.Lock()
    // Lock so only one goroutine at a time can access the map c.v.
    defer c.mux.Unlock()
    return c.v[key]
}

func main() {
    c := SafeCounter{v: make(map[string]int)}
    for i := 0; i < 1000; i++ {
        go c.Inc("somekey")
    }

    time.Sleep(time.Second)
    fmt.Println(c.Value("somekey"))
}

10. 練習:Webクローラ

この演習では、ウェブクローラ( web crawler )を並列化するため、Goの並行性の特徴を使います。

同じURLを2度取ってくることなく並列してURLを取ってくるように、 Crawl 関数を修正してみてください(注1)。

補足: 工夫すれば Crawl 関数のみの修正で実装できますが、無理に Crawl 関数内部に収める必要はありません。

ひとこと: mapにフェッチしたURLのキャッシュを保持できますが、mapだけでは並行実行時の安全性はありません!

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)
}

// Crawl uses fetcher to recursively crawl
// pages starting with url, to a maximum of depth.
func Crawl(url string, depth int, fetcher Fetcher) {
    // TODO: Fetch URLs in parallel.
    // TODO: Don't fetch the same URL twice.
    // This implementation doesn't do either:
    if depth <= 0 {
        return
    }
    body, urls, err := fetcher.Fetch(url)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("found: %s %q\n", url, body)
    for _, u := range urls {
        Crawl(u, depth-1, fetcher)
    }
    return
}

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

11. Where to Go from here...

この Goのドキュメント は、 始める にはとても良いものです。 ここにはリファレンスやチュートリアル、ビデオなどがあります。

Goのコードの構成や、動かし方を学ぶためには、 この動画 を見てください。また、 How to Write Go Code を読んでください。

もし、標準ライブラリに関してヘルプが必要なら、 パッケージリファレンス を見てください。 言語自身に関してのヘルプは、 Go言語仕様 がとても参考になります。

Goの並行性のモデルについてもっと調べてみたいなら、 Go Concurrency Patterns (slides) や、 Advanced Go Concurrency Patterns (slides) を見たり、 codewalk: Share Memory by Communicating を読んでみてください。

Webアプリケーションをはじめてみるなら、 A simple programming environment (slides) を見たり、 チュートリアル: Writing Web Applications を読んでみてください。

First Class Functions in Go では、Goの関数型の興味深い側面を教えてくれます。

Go Blog には、Goに関する素晴らしい記事の多くのアーカイブがあります。

もちろん、 golang.org も見てください。

翻訳: @atotto