Go言語のsyncパッケージの基本!WaitGroup・Mutexの使い方をやさしく解説
生徒
「先生!Go言語で並行処理をするときに、syncパッケージってよく出てきますけど、何をするものなんですか?」
先生
「良いところに気づきましたね。Go言語のsyncパッケージは、複数の処理(goroutine)を安全に制御するための道具箱なんです。中でもよく使うのがWaitGroupとMutexです。」
生徒
「なるほど!でもそれぞれ、どんな場面で使うんですか?」
先生
「それでは、実際のコードを見ながら、WaitGroupとMutexの基本を一緒に学んでいきましょう!」
1. syncパッケージとは?
Go言語のsyncパッケージは、「スレッドセーフ(安全な並行処理)」を実現するための仕組みを提供しています。Goでは、同時に複数の処理を実行するために「goroutine(ゴールーチン)」という軽量スレッドを使います。しかし、同じ変数を複数のgoroutineが同時に操作すると、結果が壊れてしまうことがあります。
そのため、データの整合性を守りながら並行処理を行うために登場するのが、sync.WaitGroupとsync.Mutexです。
2. WaitGroupでgoroutineの完了を待つ
WaitGroupは、「複数のgoroutineが終わるまで待つ」ための仕組みです。例えば、3つの処理を同時に実行して、全部終わってから次の処理に進みたい場合に使います。
使い方はシンプルで、以下の3つのメソッドを覚えればOKです。
Add(n):待つゴルーチンの数を登録するDone():ゴルーチンが終了したことを通知するWait():すべてのゴルーチンが終了するまで待機する
WaitGroupの実装例
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 終了を通知
fmt.Printf("Worker %d: 開始\n", id)
time.Sleep(1 * time.Second) // 疑似的な作業時間
fmt.Printf("Worker %d: 終了\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait() // すべてのworkerが終わるまで待つ
fmt.Println("すべての作業が完了しました!")
}
実行結果
Worker 1: 開始
Worker 2: 開始
Worker 3: 開始
Worker 1: 終了
Worker 2: 終了
Worker 3: 終了
すべての作業が完了しました!
このようにWaitGroupを使うことで、複数のgoroutineが終わるのを安全に待つことができます。
3. Mutexでデータ競合を防ぐ
Mutex(ミューテックス)は、「同時に1つのgoroutineしか変数を触れないようにする鍵」のようなものです。複数のgoroutineが同じ変数を同時に更新すると、意図しない結果になることがあります。これを防ぐために使います。
Mutexを使わない場合の例
package main
import (
"fmt"
"time"
)
var count int
func increment() {
for i := 0; i < 1000; i++ {
count++
}
}
func main() {
for i := 0; i < 5; i++ {
go increment()
}
time.Sleep(1 * time.Second)
fmt.Println("カウント結果:", count)
}
実行結果(例)
カウント結果: 4378
本来なら「1000 × 5 = 5000」になるはずなのに、結果がバラバラになります。これは複数のgoroutineが同時にcountを更新してしまうためです。
Mutexを使った正しい例
package main
import (
"fmt"
"sync"
"time"
)
var (
count int
mu sync.Mutex
)
func increment() {
for i := 0; i < 1000; i++ {
mu.Lock() // ロックをかける
count++
mu.Unlock() // ロックを外す
}
}
func main() {
for i := 0; i < 5; i++ {
go increment()
}
time.Sleep(1 * time.Second)
fmt.Println("カウント結果:", count)
}
実行結果(例)
カウント結果: 5000
mu.Lock()で「この変数は今使っていますよ」と宣言し、他のgoroutineが触れないようにします。終わったらmu.Unlock()でロックを外します。これでデータの整合性を保ちながら安全に並行処理ができます。
4. WaitGroupとMutexを組み合わせる
実際のプログラムでは、WaitGroupとMutexを組み合わせて使う場面が多いです。WaitGroupで「全ゴルーチンの完了を待ち」、Mutexで「変数の安全な更新」を行うという流れです。
package main
import (
"fmt"
"sync"
)
var (
count int
mu sync.Mutex
)
func add(wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 1000; i++ {
mu.Lock()
count++
mu.Unlock()
}
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go add(&wg)
}
wg.Wait()
fmt.Println("最終カウント:", count)
}
実行結果
最終カウント: 5000
このように、sync.WaitGroupとsync.Mutexを使うことで、複数のゴルーチンを安全かつ正確に制御できます。Go言語で並行処理を行うときには欠かせない基本テクニックです。