post Image
Go での非同期処理 その1

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 発生したらどうやってデバッグするんだろう?とかわかっていないこともあるので、次回以降掘り下げたい。

リファレンス


『 Go 』Article List
Category List

Eye Catch Image
Read More

Androidに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

AWSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Bitcoinに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

CentOSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

dockerに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

GitHubに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Goに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Javaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

JavaScriptに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Laravelに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Pythonに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Rubyに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Scalaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Swiftに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Unityに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Vue.jsに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Wordpressに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

機械学習に関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。