はじめに
Goで並列処理を行う際に複数のgoroutine間で同一の変数を扱いたい場合、宣言した変数に単純にアクセスするだけだと意図しない動作をする場合があります。
その場合、mutexを使って変数への排他制御を行うことで正しく動作させることができるので、mutexの使い方を紹介します。
mutexとは
mutexはsyncパッケージに定義されている構造体で、排他制御を行う際に用いられます。
あるgoroutine内でmutexのLockメソッドを実行すると、別のgoroutine内からはそのmutexに対して再度Lockメソッドを実行することはできず、mutexのUnlockメソッドが呼ばれるまで待機します。
mutexを扱う上での注意点として、mutexのLockメソッドを初めて実行した後は値をコピーしてはいけません。
mutexを使った排他制御
失敗するケース
最初に、mutexを使わずにある変数の値を並列で変更し、意図した値にならない場合のコードを紹介します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup var count1, count2 int for i := 0; i < 5; i++ { wg.Add(1) // 非同期で実行 go func() { defer wg.Done() for j := 0; j < 1000; j++ { count1++ } }() // 同期で実行 for j := 0; j < 1000; j++ { count2++ } } wg.Wait() // count1: 3698, count2: 5000 fmt.Printf("count1: %d, count2: %d\n", count1, count2) } |
16行目から21行目が非同期でcount1を加算している処理になります。
このように、count1に対してgoroutineの処理の中で値を加算していますが、このコードではcount1への同時アクセスが発生し、実行のたびに毎回値が変わり、殆どの場合は値が5000になりません。
これを正しく計算できるようにするために、mutexを使ってcount1変数へのアクセスを排他制御します。
mutexを使って排他制御した場合
count1への排他制御を行ったコードがこちらです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | package main import ( "fmt" "sync" ) func main() { var wg sync.WaitGroup var count1, count2 int // Mutexの宣言 var mux sync.Mutex for i := 0; i < 5; i++ { wg.Add(1) // 非同期で実行 go func() { defer wg.Done() for j := 0; j < 1000; j++ { // ロック mux.Lock() count1++ // アンロック mux.Unlock() } }() // 同期で実行 for j := 0; j < 1000; j++ { count2++ } } wg.Wait() // count1: 5000, count2: 5000 fmt.Printf("count1: %d, count2: %d\n", count1, count2) } |
まず、13行目でmutexを宣言しています。この変数に対して、goroutine内の23行目でロックを掛け、count1を加算したあとで28行目でアンロックをしています。
こうすることで、ロックを掛けたgoroutine以外がcount1にアクセスした場合、mutexがアンロックされるまで待機し、アンロックされてからロックを掛けることで、常にロックを掛けた1つのgoroutine以外はアクセスができなくなり、値が正しく計算されるようになります。
構造体へmutexを埋め込む
mutexを使って排他制御を行った例では、main関数内でmutexを定義していますが、多くのアプリケーションではmutexを構造体へ埋め込み、メソッド内でロック&アンロックを行うことが多いと思います。
ここでは、構造体へmutexを埋め込んだ例を紹介します。
まず、非同期での加算に対応したCounter構造体を定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | package value import ( "sync" "time" ) type Counter struct { value int mux sync.Mutex } func (c *Counter) Increment() { c.mux.Lock() defer c.mux.Unlock() c.value++ } func (c *Counter) Decrement() { c.mux.Lock() defer c.mux.Unlock() c.value-- } func (c *Counter) Value() int { c.mux.Lock() defer c.mux.Unlock() // 何らかの待ち時間が発生するものとする time.Sleep(5 * time.Second) return c.value } |
このように、IncremantメソッドとDecrementメソッドの開始時/終了時にmutexをロック/アンロックしています。
mutexのアンロックに関しては、このようにdeferで行うほうが漏れにくいので良いと思います。
次に、この構造体を使って非同期にカウントアップしたコードがこちらです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | package main import ( "fmt" "mutex-sample/code3/value" "sync" ) func main() { var wg sync.WaitGroup var count int var counter value.Counter for i := 0; i < 5; i++ { wg.Add(1) // 非同期で実行 go func() { defer wg.Done() for j := 0; j < 1000; j++ { counter.Increment() } }() // 同期で実行 for j := 0; j < 1000; j++ { count++ } } wg.Wait() // count1: 5000, count2: 5000 fmt.Printf("count1: %d, count2: %d\n", counter.Value(), count) } |
mutexを構造体に埋め込んだ場合でも問題なく動作しました。
RWMutexを使う
RWMutexはmutexと同様に排他制御で用いられます。
両者の違いは、mutexがLockメソッドによってReadもWriteもブロックするのに対して、RWMutexにはLockメソッドとRLockメソッドの2種類のロック方法があり、RLockメソッドでロックした場合は、他のgoroutineからのRLockをブロックしないといった違いがあります。
これにより、Readの待ち時間を減らせる場合があります。
先程のCounter構造体のmutexをRWMutexに置き換えた場合の実装はこのようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | package value import ( "sync" "time" ) type Counter2 struct { value int mux sync.RWMutex } func (c *Counter2) Increment() { c.mux.Lock() defer c.mux.Unlock() c.value++ } func (c *Counter2) Decrement() { c.mux.Lock() defer c.mux.Unlock() c.value-- } func (c *Counter2) Value() int { c.mux.RLock() defer c.mux.RUnlock() // 何らかの待ち時間が発生するものとする time.Sleep(5 * time.Second) return c.value } |
Counter構造体との違い、Valueメソッド内でのロックの取得がLockメソッドからRLockメソッドに変わっています。
これにより、あるgoroutineでValueメソッドを実行したとしても、別のgoroutineのValueメソッドの実行をブロックすることはなくなります(IncrementメソッドとDecrementメソッドの実行はブロックされます)。
そのため、値の更新を行わなずに値の取得のみを行うgoroutineの実行を高速化することができます。
さいごに
mutexを使った排他制御について紹介しました。mutexの内部実装について知りたい方に向けて参考になるURLを記載しますので、よろしければご覧ください。
https://speakerdeck.com/ffjlabo/sync-dot-mutexnoshi-zu-miwoli-jie-suru