13
6

More than 5 years have passed since last update.

goroutineとチャネルの動きを図を使って理解する(和訳)

Posted at

はじめに

先週のGolang Weeklyで流れてきた下記の記事が、groutineチャネルの動きについてわかりやすそうだったので英語の勉強も兼ねて和訳してみました。
変な訳し方もあったら指摘いただけると幸いです。
A visual introduction to golang concurrency and goroutines
和訳中に利用する画像やコードは全て上記からの引用です。

A visual introduction to golang concurrency and goroutines

Golangは、goroutineを利用した驚くべき並行(≠並列)システムとして知られている言語である。
Golangはgoroutineとチャネルによって平行性を実現している。

Prerequisites

  • go構文の基本的な理解

What are Goroutines?

goroutineはスレッドとして考えることもできるが、異なる点がいくつかある。

  • goroutineは、平均8kB軽い
  • goroutineは、チャネルと通信するが、スレッド通信の方が複雑
  • goroutineのスケジューリングは、実行時にgoによって管理されている
  • goroutineには、アイデンティティがない
  • goroutineは、実際のOSスレッド上で実行され、その数はGOMAXPROCSによりコントロールされている

Goroutine states

このチュートリアルでは、goroutineが次のうちの1つの状態に常にいる状態と考えると便利である。
goroutineの状態図

  • Waiting: goroutineは、ブロッキングステートメントにブロックされていないが、スケジューラによってまだ実行されていない状態。
  • Running: goroutineは、スケジューラによって実行されている状態。
  • Blocking: goroutineは、ブロッキング操作(チャネルやスリープ、ネットワーク操作等)によってブロックされている。そしてスケジューラは、次に利用可能なgoroutineへと遷移する。
  • Executed: goroutineは、完全に実行され、終了する。

Firing a goroutine

このコードを見てください。

func main(){
    s := "main"
    go side()
    for _,item := range s{
        fmt.Println(string(item))
    }
}

func side(){
    s := "goroutine"
    for _,item := range s{
        fmt.Println(item)
    }
}
実行結果
main
何が起きているか?

goroutineの状態図
side()がgoroutineとして実行するために呼ばれているが、まだそこからの出力は受け取っていない。なぜか?
goroutine()はまだready状態ではなく、スケジューラがアプリケーションの実行を通して実行しなかった。

もしこのように変えると:

...
func main(){
    s := "main"
    go side()
    for _,item := range s{
        fmt.Println(string(item))
    }
    time.Sleep(time.Millisecond)
}
...
これが起きる

goroutineの状態図
メインのgoroutineでブロッキングステートメントであるtime.Sleepが呼ばれ、goroutineがblocked状態になる。
それはgoスケジューラが、実行する準備ができている状態のスケジュールされたgoroutineを実行することを可能にする。この場合は、goroutine()が実行される。

これから、スケジュールされるまでgoがgoroutineを実行しないことがわかる。
そして、どのようにgoroutineを実行するのか?他の全てのgoroutineを使うことで、それを実行する、もしくはブロックする前に並べられる。

Multiple goroutines.

下記のコード考える:

func main(){
    go sayWord("goroutine1")
    go sayWord("goroutine2")
    sayWord("Main")
    time.Sleep(time.Millisecond)
    fmt.Println("End of main")
}

func sayWord(word string){
    for i := 0;i< 2; i++ {
        fmt.Println(word)
    }
}
実行結果
Main
Main
goroutine1
goroutine1
goroutine2
goroutine2
End of main

これが起こっている:
goroutineの状態図

Channels Intro

goroutine間のパイプラインとしてチャネルを考える。チャネルは、(1種類の)データを送信でき、そこから読み取ることもできる。

このチュートリアルでは緑矢印がチャネルを表している

Declaring a channel

チャネルは次の構文で生成できる。

pipe := make(chan string)

これでデータを送受信できる。

//Send hello to it
pipe <- "Hello"
go goroutine(pipe)
//Receive hello from it
func goroutine(pipe chan string){
 hello := <- pipe   
}

Unidirectional Channel

初期化時の矢印の方向で、チャネルをインプットもしくはアウトプット専用としても使うことができる。

func goroutine(in <-chan string, out chan<- string){
    received := <-in
    out <- "processed"
}

Channels are blocking by default.

データにchannel <- "Hi"が書きこまれたとき、goroutineは他のgroutineがそのチャネルから読み込むまでブロックされる。

NOTE: もし他のgoroutineが読み込まなければ、デッドロックが起きてプログラムがクラッシュする。

同様に、もし何もないのにチャネルからデータを読み込もうとしたら、goroutineは他のgoroutineがチャネルにデータを送信するまでブロックされる。

下記のコードを考えてみる:

func main() {
    pipe := make(chan string)
    go sayWord(pipe)
    fmt.Println("start of main")
    pipe <- "Hello"
    fmt.Println("End of main")

}

func sayWord(in <-chan string) {
    fmt.Println(<-in)
    fmt.Println("Data received in sayWord")
}
実行結果
start of main
Hello
Data received in sayWord
End of main

これが起きている:
goroutineの状態図

  • maingoroutineは、データがバッファがない状態のパイプチャネルに送信されるまで、実行する。(ブロッキング処理)
  • saywordはスケジューラによって実行され、一度mainを読んでいるデータがレディ状態に遷移する。
  • mainはその実行を完了する。

Buffered channels

バッファのあるチャネルはmake()コマンドの第2引数でバッファサイズを指定することで作成される。

バッファのあるチャネルとないチャネルの違いは、下記を含んでいる。

  • バッファのあるチャネルにデータをプッシュしても、データが一杯になるまではgoroutineがブロックされない。
  • バッファのないチャネルからのデータ読み込みは、空になるまではノンブロッキング状態となる。

バッファのないチャネルはmake(chan T, 0)と同じものと考えられる。

Sample code

func main() {
    pipe := make(chan int,3)
    go consoleLog(pipe)
    for i := 0; i < 3; i ++{
        pipe <- i
    }
    fmt.Println("Done")
}

func consoleLog(in <-chan int) {
    //Infinite loop to always read channel when running
    for{
        fmt.Println(strconv.Itoa(<-in))
    }
}
実行結果
Done

見たとおり、goroutineは実行されない。なぜなら、main関数のforループで3つの値がcapが3のバッファに追加されるだけで、mainのgoroutineはブロックされること無く実行されるため。

4つの値を追加した場合は:

func main() {
    pipe := make(chan int,3)
    go consoleLog(pipe)
    for i := 0; i < 4; i ++{
        pipe <- i
    }
    fmt.Println("Done")
}
...
実行結果
0
1
2
3
Done
  • maingoroutineは4回目のチャネルへのプッシュでブロックされる。
  • コントロールは、チャネルを空にする無限ループを実行するconsoleLoggoroutineに与えられる。
  • consoleLoggoroutineは、空のチャネルから読み込もうとするとブロックする。
  • コントロールは、mainに戻り、残りが実行されて終了する。

Range

range句はさっきの無限ループの代わりに、下記コードのように使うことができる。

func sayWord(in <-chan int) {
    //Infinite loop to always read channel when running
    for item := range in{
        fmt.Println(strconv.Itoa(item))
    }
}

結果はさっきと同じものが出力される。

Select

selectは、チャネルのswitchと似ている。selectは下記を行う。

  • 最初のノンブロッキングのものを実行する。
  • もし1つ以上がノンブロッキングであれば、ランダムに1つを実行する。
  • 全てがブロックされている場合、いずれかが解除されるまで全てをブロックする。defaultがない限りは。
func main() {
    pipe := make(chan string, 2)
    second := make(chan string, 2)
    go goroutines(second, time.Second*3,"First goroutine")
    go goroutines(pipe, time.Second*2,"Second goroutine")

    select {
    case res := <-pipe:
        fmt.Println(res)
    case res := <-second:
        fmt.Println(res)
    }
}

func goroutines(out chan<- string, wait time.Duration, name string) {
    time.Sleep(wait)
    out <- name
}
実行結果
Second goroutine

起きていることをブレークダウンすると:
goroutineの状態図

Default case

もし、上のコードを下記の通り更新すると、

func main() {
    pipe := make(chan string, 2)
    second := make(chan string, 2)
    go goroutines(second, time.Second*3,"First goroutine")
    go goroutines(pipe, time.Second*2,"Second goroutine")

    select {
    case res := <-pipe:
        fmt.Println(res)
    case res := <-second:
        fmt.Println(res)
    default:
        fmt.Println("None are ready")
    }
}

実行結果は、None are readyとなるはず。

利用可能なdefaultがなければ、(もし全てがブロックされていたら)selectはブロックする。defaultがあれば、それがすぐに実行される。

Conclusion.

この記事は、goroutineや、goroutineの通信、それらがどのようにスケジュールされているかを理解するのに役立つだろう。goroutineがチャネルをどのように上手く調整するかを理解できるように、チャネルの内部の動きに関するフォローアップ記事を書く予定である。(どう訳すかわからん...)

それまで、goroutineを試したり遊んだりして、異なる結果を出してみよう。もしくはGOMAXPROCSをCPUで利用可能なコア数に設定して、並列処理を試してみよう(注意が必要)。

おわりに

上手く訳せなかったところも多く、これからの英語の勉強も必須だなと感じました。
内容自体は、コードや図もあるのでわかりやすく、理解できたと思います。
今までこうした動きは、何となくこう動いているんだろうなとしか考えたことがなかったので新鮮です。
こういった内部ロジックを理解することは、パフォーマンスにも影響していくので、引き続き勉強していきたいです。

13
6
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
13
6