O'Reilly Japan - Go言語による並行処理


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回繰り返して実行してみる。

クロージャ - Wikipedia


これは実行結果を書かずともインクリメントした結果が出力されることはイメージしやすい。


このコードに対して、

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