最近、Go 言語高性能プログラミングを学んでいると、sync.Cond
条件変数という概念に出会い、少し理解するのが難しかったです。資料を調べた後、少し理解できたので、ここに記録します。
概念#
sync
パッケージは、条件変数の型であるsync.Cond
を提供しており、共有変数へのアクセスを調整するために、ミューテックスや読み書きロックと組み合わせて使用することができます。主な機能は、関連する共有リソースの状態が変化したときに、他のブロックされたゴルーチンに通知することです。
定義#
sync.Cond
は、以下のように定義される構造体です:
// Each Cond has an associated Locker L (often a *Mutex or *RWMutex),
// which must be held when changing the condition and
// when calling the Wait method.
//
// A Cond must not be copied after first use.
type Cond struct {
noCopy noCopy
// L is held while observing or changing the condition
L Locker
notify notifyList
checker copyChecker
}
この型は、sync
パッケージのfunc NewCond(l Locker) *Cond
関数を使用して作成することができます。
次の 3 つのメソッドがあります:
-
func (c *Cond) Broadcast()
c
を待っているすべてのゴルーチンを起こします。 -
func (c *Cond) Signal()
c
を待っているゴルーチンを 1 つだけ起こします(存在する場合)。 -
func (c *Cond) Wait()
c.L
のロックを自動的に解除し、ゴルーチンの実行を一時停止し、起こされるまで待機します。この関数は、戻る前にc.L
を再度ロックします。
応用シナリオ#
上記の説明からわかるように、この構造体の主な使用法は、複数のゴルーチンがWait
を呼び出して待機し、1 つのゴルーチンが待機しているゴルーチンを起こす場合です。つまり、複数のゴルーチンが待機し、1 つのゴルーチンが通知するシナリオによく使用されます。
生活の小さな例を挙げてみましょう:
複数の学生が食堂に到着し、まだ食事が準備されていない場合、学生たちは食事をするために食事が準備されるのを待たなければなりません。
上記のシナリオを Go の条件変数で記述すると、次のようなコードになります:
package main
import (
"fmt"
"sync"
"time"
)
var (
ok = false // 食事が準備されたかどうか
food = "" // 食べ物の名前
rwMutex = &sync.RWMutex{} // 読み書きロック、食べ物の名前の変更をロックするために使用
cond = sync.NewCond(rwMutex.RLocker()) // 条件変数は読み書きロックの読み込みロックを使用する
)
func makeFood() {
// 食事の準備には書き込みロックを使用します(もちろん、1つの食事のゴルーチンしかないため、このロックは実際には意味がありません)
rwMutex.Lock()
defer rwMutex.Unlock()
fmt.Println("食堂が食事の準備を始めました!")
time.Sleep(3 * time.Second)
ok = true
food = "魚香肉絲"
fmt.Println("食堂が食事の準備を完了しました!")
cond.Broadcast()
}
func waitToEat() {
cond.L.Lock()
defer cond.L.Unlock()
for !ok {
cond.Wait()
}
fmt.Println("やっと食べ物を食べることができました。今回は", food, "です")
}
func main() {
for i := 0; i < 3; i++ {
go waitToEat()
}
go makeFood()
time.Sleep(4 * time.Second)
}
このプログラムを実行すると、次の結果が得られます:
食堂が食事の準備を始めました!
食堂が食事の準備を完了しました!
やっと食べ物を食べることができました。今回は 魚香肉絲 です
やっと食べ物を食べることができました。今回は 魚香肉絲 です
やっと食べ物を食べることができました。今回は 魚香肉絲 です
他の概念との違い#
チャネル#
周知のように、チャネル
もゴルーチンの同期に使用される Go の概念ですが、sync.Cond
とは異なり、チャネル
は通常 1 対 1 の通信に使用されます。先ほどの例を引き継いで、今回のシナリオは次のようになります:
一人の学生が食堂に到着し、まだ食事が準備されていない場合、その学生は食事が準備されるのを待たなければなりません。
チャネル
で記述すると:
package main
import (
"fmt"
"time"
)
var (
food = ""
ch = make(chan struct{})
)
func makeFood() {
fmt.Println("食堂が食事の準備を始めました!")
time.Sleep(3 * time.Second)
food = "魚香肉絲"
fmt.Println("食堂が食事の準備を完了しました!")
ch <- struct{}{}
}
func waitToEat() {
<-ch
fmt.Println("やっと食べ物を食べることができました。今回は", food, "です")
}
func main() {
go waitToEat()
go makeFood()
time.Sleep(4 * time.Second)
}
実行結果は次のようになります:
食堂が食事の準備を始めました!
食堂が食事の準備を完了しました!
やっと食べ物を食べることができました。今回は 魚香肉絲 です
sync.WaitGroup#
sync.WaitGroup
もゴルーチンの同期に使用されますが、応用シナリオはsync.Cond
とは逆で、後者は複数のゴルーチンが待機し、1 つのゴルーチンが通知する場合によく使用されますが、前者は1 つのゴルーチンが複数のゴルーチンの完了を待機する場合によく使用されます。
引き続き、上記の例を使用します:
一人の学生が食堂に到着し、複数の窓口がまだ食事の準備が完了していない場合、その学生は各窓口が食事の準備を完了した後に各窓口から一つずつ食事を注文します。(少し現実離れしていますが、笑)
コードは次のようになります:
package main
import (
"fmt"
"sync"
"time"
)
var (
wg sync.WaitGroup
)
func makeFood() {
fmt.Println("窓口が食事の準備を始めました!")
time.Sleep(3 * time.Second)
fmt.Println("窓口が食事の準備を完了しました!")
wg.Done()
}
func waitToEat() {
wg.Wait()
fmt.Println("おお、各窓口から一つずつ!")
}
func main() {
for i := 0; i < 3; i++ {
wg.Add(1)
go makeFood()
}
go waitToEat()
time.Sleep(4 * time.Second)
}
実行結果は次のようになります:
窓口が食事の準備を始めました!
窓口が食事の準備を始めました!
窓口が食事の準備を始めました!
窓口が食事の準備を完了しました!
窓口が食事の準備を完了しました!
窓口が食事の準備を完了しました!
おお、各窓口から一つずつ!
まとめ#
上記は個人的な浅い理解ですので、間違いがあればコメントで指摘していただければ幸いです。ありがとうございます!