Go言語で並行処理についての本が出版されたので購入して読んでいる。
この本よりも前に
Lambda Note Goならわかるシステムプログラミング
Goでシステムプログラミングに触れる本でも並行処理について記載されていて、
syncパッケージのWaitGroupやMutexが大事だという記載があり、
説明文からもこれらが大事だということはわかるんだけれども、
普段並行処理を書かない私にとってしっくりこなかった。
というわけで今回は自分用のメモとして
サンプルコードを作って処理を追ってみることにした。
package main import ( "fmt" ) func closure() func() { var i int return func() { i++ fmt.Printf("now:%d\n", i) } } func main() { c := closure() for i := 0; i < 10000; i++ { c() } }
はじめに実行の度に1加算して出力するクロージャ関数を用意して、
それをエントリポイント(main関数)内で10000回繰り返して実行してみる。
これは実行結果を書かずともインクリメントした結果が出力されることはイメージしやすい。
このコードに対して、
package main import ( "fmt" ) func closure() func() { var i int return func() { i++ fmt.Printf("now:%d\n", i) } } func main() { c := closure() for i := 0; i < 10000; i++ { go c() } }
クロージャ関数の前にgoを付けて、並行処理として実行してみると、
now:8566 now:8567 now:8573 now:8574 now:8570 now:8577 now:8579 now:8577 now:8582 now:8583
※最後の10個の結果
こんな感じで出力の数値がばらつく。
更にどうやら最後(10000を出力)まで到達していないみたいだ。
これは並行処理としてc()を実行したけれども
新たに追加したゴルーチン(関数の前にgoとつけることで生成)の実行よりも前に
エントリポイントの方が先に終わってしまったことが要因らしい。
ここで追加したゴルーチンを確実に終了させるためにsync.WaitGroupを利用するらしい。
追加したコードは下記の通り。
package main import ( "fmt" "sync" ) var wg sync.WaitGroup func closure() func() { var i int return func() { defer wg.Done() i++ fmt.Printf("now:%d\n", i) } } func main() { c := closure() for i := 0; i < 10000; i++ { wg.Add(1) go c() } wg.Wait() }
mainパッケージのスコープでsync.WaitGroup型の変数を追加して、
c()関数を実行する前にwg.Add(1)で実行待ちを登録
直後にc()関数を追加し、
c()関数の中で関数が実行される度に最後にwg.Done()を追加して、
wg.Add(1)で指定した待ちタスクの数を1減らす。
最後にエントリポイントの最後でwg.Wait()で実行待ちのタスクを最後まで消化するまで待つという指示を追加して実行してみたところ、
now:9943 now:9944 now:2351 now:8960 now:9933 now:5871 now:5873 now:9950 now:9899 now:8956
※最後の10個の結果
実行された数字が10000に近いものが出力されるようになった。
ただし、まだ順番はインクリメントになっていない。
続いて、sync.Mutexを試してみる。
専門的な話は冒頭で紹介した本に委ねるとして、
簡単に説明すると関数前にgoを付けることで並行処理として登録した関数で、
あるゴルーチンが実行されたら、その実行を最後まで待った上で次のゴルーチンを実行する仕組みを組み込むことが出来る。
※原始的な方法として、クロージャ関数内のi++の後でスリープを使う
実際に使ってみる。
package main import ( "fmt" "sync" ) var mutex sync.Mutex func closure() func() { var i int return func() { mutex.Lock() defer mutex.Unlock() i++ fmt.Printf("now:%d\n", i) } } func main() { c := closure() for i := 0; i < 10000; i++ { go c() } }
上記のコードのようにクロージャ関数内で関数が実行される最初の行でロックして、関数の終了時にロックを解除することで、
now:3051 now:3052 now:3053 now:3054 now:3055 now:3056 now:3057 now:3058 now:3059 now:3060
※最後の10個の結果
実行結果がインクリメントになっている。
ただし、最後(10000)には全然到達していない。
というわけで、
sync.WaitGroupとsync.Mutexを組み合わせて使ってみると、
package main import ( "fmt" "sync" ) var mutex sync.Mutex var wg sync.WaitGroup func closure() func() { var i int return func() { mutex.Lock() defer mutex.Unlock() defer wg.Done() i++ fmt.Printf("now:%d\n", i) } } func main() { c := closure() for i := 0; i < 10000; i++ { wg.Add(1) go c() } wg.Wait() }
実行結果は、
now:9991 now:9992 now:9993 now:9994 now:9995 now:9996 now:9997 now:9998 now:9999 now:10000
※最後の10個の結果
上記のようにインクリメントでしかも最後(10000)まで出力されるようになった。
sync - The Go Programming Language