Go の sync.Cond で goroutine 間の待機と通知を行う

sync.Cond は、ある条件が満たされるまで goroutine を待機させ、条件が整ったら通知して再開させる仕組みを提供します。複数の goroutine が特定のイベントを待つ場面で活躍します。

sync.Cond の基本

sync.Cond は Mutex と組み合わせて使います。sync.NewCond に Mutex を渡して生成し、Wait で待機、Signal または Broadcast で通知を送ります。

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var mu sync.Mutex
    cond := sync.NewCond(&mu)
    ready := false

    // 待機する goroutine
    go func() {
        mu.Lock()
        for !ready {
            cond.Wait() // 条件が満たされるまで待機
        }
        fmt.Println("条件が満たされました!")
        mu.Unlock()
    }()

    time.Sleep(time.Second)

    // 条件を満たして通知
    mu.Lock()
    ready = true
    cond.Signal() // 待機中の goroutine を1つ起こす
    mu.Unlock()

    time.Sleep(time.Second)
}

Wait を呼ぶと、内部でロックを解放して待機状態に入ります。通知を受けると再びロックを取得してから処理を再開するため、条件のチェックと更新が安全に行えます。

Signal と Broadcast の違い

Signal は待機中の goroutine を1つだけ起こしますが、Broadcast はすべての待機中 goroutine を起こします。

Signal

待機中の goroutine を1つだけ起こす。1対1の通知に適している。

Broadcast

待機中のすべての goroutine を起こす。複数の goroutine に同時通知したいときに使う。

複数のワーカーが同じ条件を待っていて、一斉に動き出してほしい場合は Broadcast が適しています。

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var mu sync.Mutex
    cond := sync.NewCond(&mu)
    started := false
    var wg sync.WaitGroup

    // 3つのワーカーが開始を待つ
    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            mu.Lock()
            for !started {
                cond.Wait()
            }
            mu.Unlock()
            fmt.Printf("ワーカー%d: 開始しました\n", id)
        }(i)
    }

    time.Sleep(time.Second)

    // 全ワーカーに通知
    mu.Lock()
    started = true
    cond.Broadcast()
    mu.Unlock()

    wg.Wait()
}

Wait は必ずループで囲む

Wait から戻ったとき、条件が本当に満たされているとは限りません。スプリアスウェイクアップ(偽の起床)が起こる可能性があるため、必ず for ループで条件をチェックし続ける必要があります。

// 正しい使い方
mu.Lock()
for !condition {
    cond.Wait()
}
// 条件が満たされた後の処理
mu.Unlock()

if で1回だけチェックするのは危険です。条件が満たされていない状態で処理が進んでしまう可能性があるためです。

実践例:キューの待機

sync.Cond はプロデューサー・コンシューマーパターンでよく使われます。キューが空のときはコンシューマーを待機させ、データが追加されたら通知するという流れです。

package main

import (
    "fmt"
    "sync"
    "time"
)

type Queue struct {
    items []int
    mu    sync.Mutex
    cond  *sync.Cond
}

func NewQueue() *Queue {
    q := &Queue{}
    q.cond = sync.NewCond(&q.mu)
    return q
}

func (q *Queue) Push(item int) {
    q.mu.Lock()
    q.items = append(q.items, item)
    q.cond.Signal() // 待機中のコンシューマーに通知
    q.mu.Unlock()
}

func (q *Queue) Pop() int {
    q.mu.Lock()
    for len(q.items) == 0 {
        q.cond.Wait() // データが来るまで待機
    }
    item := q.items[0]
    q.items = q.items[1:]
    q.mu.Unlock()
    return item
}

func main() {
    q := NewQueue()

    // コンシューマー
    go func() {
        for i := 0; i < 3; i++ {
            item := q.Pop()
            fmt.Printf("取得: %d\n", item)
        }
    }()

    // プロデューサー
    for i := 1; i <= 3; i++ {
        time.Sleep(500 * time.Millisecond)
        q.Push(i)
        fmt.Printf("追加: %d\n", i)
    }

    time.Sleep(time.Second)
}

sync.Cond は channel でも代替できる場面が多いですが、複数の goroutine への一斉通知や、複雑な条件での待機が必要な場合に真価を発揮します。