57
35

More than 5 years have passed since last update.

Go での非同期処理 その1

Posted at

Go での非同期処理がいまいちわかっていなかったので、調べてみた。 Go の並列処理の基本は、「メモリをシェアして、コミュニケーションをとるのではなく、コミュニケーションをとって、メモリをシェアする」と言うコンセプトらしい。一瞬なんのことかわからないが、試してみよう。

1. 非同期処理を、複数は知らせて、全部終わったら、何らかの処理をする。

C# や TypeScript だと、 async/await が便利すぎていい感じだが、Go には go routine と、 channel が存在する。かなりかっこいい感じで並列処理がかける。 REST-API からデータを取ってきたかったり、IO関係の処理だと、並列処理を行いたいだろう。次のは、2つの web サーバーから並列処理で、データを取得して、両方が読み終わったら、内容を表示するサンプルだ。

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
)

func GetContent(url string, c chan string) {
    resp, err := http.Get(url)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)

    s := string(body[:])

    if err != nil {
        fmt.Println(err)
        return
    }
    c <- s
}

func main() {
    fmt.Println("Hello, playground")
    c := make(chan string)
    go GetContent("https://www.bing.com/", c)
    go GetContent("https://www.yahoo.co.jp", c)
    result01, result02 := <-c, <-c
    fmt.Println(result01)
    fmt.Println("********-----------")
    fmt.Println(result02)

}

余談だが、: は、スライス 参考: Go Slices: usage and internalsに使われる記号で、 body[:] は、スライスの前後が省略されているので、byte[] body の body のストレージ自体を指し示している。(それを string に変換している)

ポイントを解説していこう。

1.1. Channel を作成する

非同期で実行されるメソッドは先頭に、go をつけて関数をコールする。これをgo routine と呼ぶ。

go GetContent("https://www.bing.com/", c)

ポイントは、下記の通り、channel と呼ばれるもので、メインの処理と、非同期に実行する処理で、シェアされるチャネルだ。メイン処理と、非同期処理の方で何か共有したかったら、チャネルを介して、データをやり取りする。この辺りが、「メモリをシェアして、コミュニケーションをとるのではなく、コミュニケーションをとって、メモリをシェアする」と書いてある所以だろう。下記のは、バッファーのないチャネルを作成しているが、バッファー付きも存在する。その違いはあとで解説する。

c := make(chan string)

1.2. 非同期処理側で、チャネルに書き込む

func GetContent(url string, c chan string) {
    resp, err := http.Get(url)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)

    s := string(body[:])

    if err != nil {
        fmt.Println(err)
        return
    }
    c <- s
}

前半は、引数でもらった、URL から、その Web ページを取ってきて、それをs に代入しているだけだが、その戻り値を、channel に格納している。 c <- s の箇所である。こうすると、結果が、チャネルに格納されて、メイン側から、その値にアクセスできるようになる。

    go GetContent("https://www.bing.com/", c)
    go GetContent("https://www.yahoo.co.jp", c)
    result01, result02 := <-c, <-c

メイン側では、2つの go routine を実行しているが、それぞれ、処理が始まったら、次の処理を待ち合わせない。じゃあ、awaitに相当するのはどうするかというと、3行目でやっている内容だ。

result01, result03 := <-c, <-c

<-c を実行すると、チャネルから1つの値を取ってくる。つまり、この行を終えようとしたら、チャネルから、2つの値を取ってこないといけない。だから、ここで待ち合わせがかかるのだ。もし、go GetContent("https://www.yahoo.co.jp", c) の間にロジックがあったとしても、この行までは、待ち合わせは行わない。

go では、リソースをロックする方法ではなく、Mutex と言う方法で、データを共有する。Mutexの方法は、複数の人がいるときに、おもちゃのアヒルを持っている人だけが、話をできると言うルールにしておく。アヒルを持っている人でないと話はできない。参加者に次々アヒルを渡していけば、一人だけが話をするという状態になる。
 人をスレッドに置き換えると、アヒルが、Mutex だ。つまり、Channel そのものである。(たぶんw) What is a mutex?

このようにすると、実際に、2つの非同期処理が同時に実行されて、上記の箇所で待ち合わせて、プログラムが終了する。まさに、async, await っぽいことができている。ただ、考え方は違うので慣れが必要だ。

2. バッファー付きチャネルと、バッファーなしチャネルを理解する

ちなみに、チャネルには、バッファー付きと、バッファーなしが存在する。この違いをプログラムを作って理解してみよう。


package main

import (
    "fmt"
    "time"
)

func GetInt(a int, c chan int) {
    for i := 1; i < 5; i++ {

        c <- a + i
        fmt.Println("GetInt:", i)
    }
    close(c)
}

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

    go GetInt(0, c)

    for d := range c {
        time.Sleep(time.Second * 2)
        fmt.Println("Main: ", d)
    }

}

2.1. バッッファーなし channel

このプログラムでは、go routine の方で、チャネルに、書き込み、メインの方では、2秒ほど待ってから、チャネルを読み込んでいる。これを実行するとどうなるだろう。チャネルにデータを入れたくても、バッファがないので、1つか入れれない。だから、メイン側で、読み込みが行われないと、go routine 側で、書き込みを行えない。だから、 go routine 側で1つデータが入ったら、メインで1つ読み込む、、、といった感じになる。

$ go run spike3.go
GetInt: 1
Main:  1
GetInt: 2
Main:  2
GetInt: 3
Main:  3
GetInt: 4
Main:  4

2.2. バッファ付き channel

じゃあ、バッファ付きだとどうなるだろう? バッファが2つになる。

c := make(chan int, 1)

実行すると、バッファが2つあるので、読み込みは、2件同時にできている。でも、読み込みが行われないと、バッファがあかないので、最初以降は、1件読み込まれたら、1件書き込みができると言うような形で動いている。

$ go run spike3.go
GetInt: 1
GetInt: 2
Main:  1
GetInt: 3
Main:  2
GetInt: 4
Main:  3
Main:  4

もっと豪勢にバッファを持ってみる。

c := make(chan int, 3)

予想通り、一気にバッファに ぶち込んで、一気に読み取ると言うことができている。

$ go run spike3.go
GetInt: 1
GetInt: 2
GetInt: 3
GetInt: 4
Main:  1
Main:  2
Main:  3
Main:  4

3. コンカレントと、パラレルの違い

並列処理の英語訳は何かわからないが、Concurrent と、 Parallel は意味が違う。

Stack Overflow のこの図が最高にわかりやすい。
What is the difference between concurrent programming and parallel programming?
con_and_par.jpg

go routine は、コンカレントプログラミングを実施するものなので、パラレル実行のためには、一工夫必要だ。

さっきのプログラムを改造してみる。

すごく単純だが、CPU の数を取ってきて、その数だけ、Channelのバッファを作ればいい。

package main

import (
    "fmt"
    "runtime"
)

func GetInt(a int, c chan int) {
    c <- a
    fmt.Println("GetInt", a)
}

func main() {
    numCPU := runtime.GOMAXPROCS(0)
    fmt.Println("NUMCPU:", numCPU)
    c := make(chan int, numCPU)

    for i := 0; i < numCPU-1; i++ {
        go GetInt(i*10, c)
    }
    result03, result02, result01 := <-c, <-c, <-c
    fmt.Println("Main:", result01)
    fmt.Println("Main:", result02)
    fmt.Println("Main:", result03)
}

実行結果

$ go run spike3.go
NUMCPU: 4
GetInt 20
GetInt 10
GetInt 0
Main: 10
Main: 0
Main: 20

多少順番は入れ替わっているが、予想通りの結果になっている。オリジナルのコードでは、CPUの数だけ回していたが、メインが動いているCPUがあるはずなので、3つにしてみた。

終わりに

go routine の強力さはわかってきたが、まだまだ、go routinepanic 発生したらどうやってデバッグするんだろう?とかわかっていないこともあるので、次回以降掘り下げたい。

リファレンス

57
35
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
57
35