はじめに
Go言語(golang)は並行処理を簡単に扱えることで有名ですが、その分、競合状態(race condition)に注意する必要があります。
Go言語での競合状態を具体的なサンプルコードを用いて解説し、その対処法について説明します。
競合状態(Race Condition)とは?
競合状態は、複数の並行処理が同時に共有データにアクセスする際に発生する可能性があり、これが原因で不具合や意図しない動作が発生することがあります。
例えば、下記の競合状態です。
package main
import (
"fmt"
"sync"
)
var sharedCounter int
func main() {
wg := &sync.WaitGroup{}
mu := &sync.Mutex{}
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock() // コメントアウトして、競合状態を発生させる
value := sharedCounter
value++
sharedCounter = value
mu.Unlock() // コメントアウトして、競合状態を発生させる
}()
}
wg.Wait()
fmt.Println("sharedCounter:", sharedCounter)
}
sharedCounter
という共有変数に1000個のゴルーチンがアクセスし、インクリメント操作を行います。sync.Mutex
を使ってアクセスをロックしていますが、mu.Lock()
とmu.Unlock()
の行をコメントアウトすると、競合状態が発生します。
競合状態の発生理由
競合状態が発生する理由は、複数のゴルーチンが同時にsharedCounter
にアクセスし、その値を読み書きしているためです。これにより、一部のインクリメントが上書きされ、期待した結果と異なるカウント値が出力されることがあります。
対処法
競合状態を回避する方法として、sync.Mutex
を使用して、同時に共有データにアクセスすることを制限します。この例では、mu.Lock()
でロックを取得し、mu.Unlock()
でロックを解放しています。これにより、一度に1つのゴルーチンだけが共有データにアクセスでき、競合状態が回避されます。
その他の対処法
競合状態への対処法としては、sync.Mutex
の他にも、sync.RWMutex
(読み込み専用のロックを許可)やチャネルを使用する方法もあります。チャネルは、ゴルーチン間でデータの送受信を行う仕組みで、明示的なロックを使わずに競合状態を回避できる場合があります。
package main
import (
"fmt"
"sync"
)
var sharedCounter int
func incrementCounter(wg *sync.WaitGroup, ch chan<- int) {
defer wg.Done()
ch <- 1
}
func main() {
wg := &sync.WaitGroup{}
ch := make(chan int, 1000)
go func() {
for i := 0; i < 1000; i++ {
wg.Add(1)
go incrementCounter(wg, ch)
}
wg.Wait()
close(ch)
}()
for value := range ch {
sharedCounter += value
}
fmt.Println("sharedCounter:", sharedCounter)
}
このコードでは、1000個のゴルーチンがチャネルch
に1を送信し、メインゴルーチンがチャネルから受信してsharedCounter
をインクリメントします。この方法では、競合状態が発生せず、期待通りの結果が得られます。
競合状態の検出
Go言語には、競合状態を検出するためのツールgo run -race
が付属しています。このツールを使うことで、コード内の競合状態を検出し、修正することができます。
まとめ
Go言語での競合状態は、複数のゴルーチンが同時に共有データにアクセスする際に注意が必要です。対処法としては、sync.Mutex
やチャネルを利用することで、競合状態を回避できます。また、go run -race
を用いることで、競合状態を検出しやすくなります。これらの手法を使って、安全な並行処理を実現しましょう。