Go言語の並行処理で発生するトラブル事例と解決法まとめ
生徒
「Goで複数のgoroutineを動かすと、どんなトラブルが起きるんですか?」
先生
「代表的なのは、競合状態やデッドロックです。競合状態は複数のgoroutineが同じ変数を同時に操作するときに起こります。デッドロックは、goroutineが互いに待ち状態になり処理が止まってしまうことです。」
生徒
「それを防ぐにはどうすればいいですか?」
先生
「channelやsyncを適切に使うこと、そして-raceオプションを使ったデバッグが有効です。順番に具体例を見ていきましょう。」
1. 競合状態(Race Condition)と解決法
競合状態は、複数のgoroutineが同じメモリを同時に書き換えるときに発生します。これにより意図しない結果が生じることがあります。Goではsync.Mutexやchannelを使って解決できます。
2. 競合状態のサンプルと-raceオプション
package main
import (
"fmt"
"sync"
)
func main() {
counter := 0
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
counter++ // 競合状態発生
wg.Done()
}()
}
wg.Wait()
fmt.Println("Counter:", counter)
}
このコードをgo run -race main.goで実行すると、競合状態が検出されます。Mutexを使うことで安全に更新可能です。
3. Mutexで競合状態を防ぐ方法
var mu sync.Mutex
go func() {
mu.Lock()
counter++
mu.Unlock()
wg.Done()
}()
Mutexでロックとアンロックを行うことで、同時アクセスによるデータ破損を防ぎます。
4. デッドロックとその解決法
デッドロックは、複数のgoroutineが互いに相手の処理完了を待ち続けてしまう状態です。特にchannelを使った送受信で発生しやすく、送信側と受信側が揃わない場合に起こります。
5. デッドロックの例と回避
ch := make(chan int)
go func() {
ch <- 1 // 受信側がないとデッドロック
}()
// main goroutineで受信
fmt.Println(<-ch)
受信がないまま送信すると、処理が止まります。必ず送信と受信が揃うようにgoroutineを設計することが重要です。
6. channelの閉鎖忘れによる問題
channelは使い終わったらclose()で閉じるのが安全です。閉じないと、受信側が永遠に待ってしまうことがあります。
ch := make(chan int)
go func() {
ch <- 100
close(ch) // 忘れずに閉じる
}()
for v := range ch {
fmt.Println(v)
}
7. ゴルーチンリークの防止
goroutineリークとは、終了すべきgoroutineがいつまでも動き続けることです。contextパッケージを使ってタイムアウトやキャンセルを設定することで防げます。
ctx, cancel := context.WithCancel(context.Background())
go func() {
select {
case <-ctx.Done():
return
}
}()
cancel() // goroutineを終了
8. トラブルを防ぐ設計のポイント
- 共有変数への同時アクセスにはsync.Mutexやsync.RWMutexを使う
- データの受け渡しはchannelを使い、送受信の順序を揃える
- goroutineの終了待ちはsync.WaitGroupで管理する
- goroutineリーク防止のためにcontextでキャンセルやタイムアウトを設定する
- 競合状態のチェックには
-raceオプションを活用する
これらを組み合わせることで、Goの並行処理で発生するトラブルを未然に防ぎ、安全なプログラム設計が可能です。