oinume journal

Scratchpad of what I learned

Goにおける並行処理 - goroutine編

はじめに

Goでは、goroutineとchannelが言語仕様として組み込まれているため、他の言語に比べてとても並行処理のコードが書きやすい。この2つの基本的な動作原理についてまとめた自分用のメモである。(channelについては別の記事で書く予定)

goroutineとは?

  • goroutine(ゴールーチン)とは、他のコードに対して並行に実行している関数のこと
  • goroutineはgoというキーワードを関数呼び出しの前に置くことで起動できる。簡単!
  • あらゆるGoのプログラムでは、main関数を動かすためのgoroutineが必ず存在する。つまりGoを支えるとてもプリミティブなものである。

goキーワードを使ってgoroutineを動かすとても簡単な例としては以下になる。

package main

import "fmt"

func main() {
    
go sayHello() 
   // 他の処理を続ける
} 

func sayHello() {
    fmt.Println("hello") 
}

このコードは以下の2つのgoroutineで動作している。

  1. main関数を動かしているgoroutine
  2. main関数から分岐した、sayHello関数を動かしているgoroutine

goroutineの正体

ではgoroutineとは何なのだろうか?Javaなどの他の言語をやってきた人からすると、これはスレッドとして実行されているのか?と気になるところだが、実態はスレッドではない。Go言語による並行処理 P.38では以下のように説明されている。

ゴルーチンは(他の言語にも似た並行処理のプリミティブは存在しますが)Go 特有のものです。ゴルーチンはOSスレッドではなく、また必ずしもグリーンスレッド――言語のランタイムにより管理されるスレッド――ではありません。ゴルーチンはコルーチン(coroutine)として知られる高水準の抽象化です。コルーチンは単に「プリエンプティブでない」並行処理のサブルーチン(Goでは関数、クロージャー、メソッドに相応)です。つまり、割り込みをされることがないということです。かわりに、コルーチンには一時停止や再エントリーを許す複数のポイントがあります。

ゴルーチンが独特なのは、ゴルーチンが Go のランタイムと密結合していることです。ゴルーチンは 一時停止や再エントリーのポイントを定義していません。Go のランタイムはゴルーチンの実行時の振る舞いを観察し、ゴルーチンがブロックしたら自動的に一時停止し、ブロックが解放されたら再開します。これによってある意味ゴルーチンをプリエンプティブにしていますが、ゴルーチンがブロックしたときにしか割り込みません。このようにランタイムとゴルーチンのロジックには美しい関係性がありま す。以上のことから、ゴルーチンは特殊なコルーチンと考えられます。

goroutineはGoのランタイムにより管理されるため、Goのコードから一時停止したりレジュームすることはできない(channelやcontextを使えば動作を止めるようにキャンセルすることは可能)。これがコルーチンとは違う点だと思う。また、goroutineとして関数を実行してもスレッドが新しく作られるわけではないので、スレッドよりも軽量である。

Go がゴルーチンをホストする機構は、いわゆる M:N スケジューラーと呼ばれる実装になっています。 これは M個のグリーンスレッドを N個の OS スレッドに対応させるものです。ゴルーチンはグリーンスレッドにスケジュールされます。グリーンスレッドの数よりも多い数のゴルーチンがある場合には、スケジューラーはゴルーチンを利用可能なグリーンスレッドに割り振って、これらのゴルーチンがブロックした場合には他のゴルーチンを実行するようにしています。

fork-joinモデル

Goではfork-joinモデルと呼ばれる並行処理のモデルに従っている。

  • fork: プログラムの特定の場所で子供の処理を分岐させて、親と平行に実行させること
  • join: 分岐した時点より以降に、平行処理されている親と子が再び合流すること

図で表すと以下のようになる。

ここで先ほどのsayHelloを使ったgoroutineの例を見てみる。

func main() {
    
go sayHello() 
} 

func sayHello() {
    fmt.Println("hello") 
}

sayHelloはgoroutineとして実行された後にjoinしていないので、helloと表示されずにこのプログラムは終わってしまう。ではmainに合流させるにはどうしたらよいだろうか?

一番簡単なやり方は以下のようにsync.WaitGroupを使うことである。

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    wg.Add(1) // 1. 起動するgoroutineの数を伝えるため、カウンターに1を足す
    go func() {
        defer wg.Done() // 2. カウンターから1をマイナスする
        sayHello()
    }()
    wg.Wait() // 3. ここでsayHelloのgoroutineが終わるまで待つ
    fmt.Println("finished")
}

func sayHello() {
    fmt.Println("hello") 
}

https://play.golang.org/p/-uPFeh_37mB

まとめ

これがgoroutineの基本的なことの全てである。goというキーワードを使うだけで並行処理が簡単に書けるということがわかるはず。

Go言語による並行処理

Go言語による並行処理

  • 作者: Katherine Cox-Buday,山口能迪
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2018/10/26
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る