paint-brush
Goのsync.Condを理解する:初心者向けガイド@ivanlemeshev
739 測定値
739 測定値

Goのsync.Condを理解する:初心者向けガイド

Ivan Lemeshev9m2024/04/28
Read on Terminal Reader

長すぎる; 読むには

結論として、sync.Cond は Go プログラミング言語の便利な型であり、特定の条件に基づいて goroutine 間の同期と調整を可能にします。条件変数を作成および管理する方法を提供します。条件を待機、シグナル、ブロードキャストするメソッドがあります。`sync.Cond` を使用すると、Go でより制御され同期された並行プログラムを作成できます。 sync.Cond は Go 標準ライブラリによって提供される同期プリミティブの 1 つにすぎず、その使用法は並行プログラムの特定の要件に依存することに注意してください。場合によっては、チャネルや sync.WaitGroup などの他の同期プリミティブの方が適している場合があります。
featured image - Goのsync.Condを理解する:初心者向けガイド
Ivan Lemeshev HackerNoon profile picture
0-item

導入

sync.Condのタイプと使用例、そしていつ使用するかについて説明します。

sync.Condとは何ですか?

Go プログラミング言語では、 sync.Cond条件変数を表すsyncパッケージで定義された型です。条件変数は、特定の条件が満たされるまで待機してから処理を続行できるようにすることで、goroutine を調整するために使用される同期プリミティブです。


sync.Cond型は、条件変数を作成および管理する方法を提供します。主なメソッドは 3 つあります。


  1. Wait() : このメソッドは、呼び出し元のゴルーチンを、別のゴルーチンが条件変数にシグナルを送るまで待機させます。ゴルーチンがWait()を呼び出すと、関連付けられたロックが解放され、別のゴルーチンが同じsync.Cond変数でSignal()またはBroadcast()を呼び出すまで実行が中断されます。


  2. Signal() : このメソッドは、条件変数を待機している 1 つの goroutine を起動します。複数の goroutine が待機している場合は、そのうちの 1 つだけが起動されます。どの goroutine を起動するかは任意であり、保証されません。


  3. Broadcast() : このメソッドは、条件変数を待機しているすべての goroutine を起動します。Broadcast Broadcast()が呼び出されると、待機中のすべての goroutine が起動され、続行できるようになります。


sync.Condでは、条件変数へのアクセスを同期するために、関連付けられたsync.Mutexが必要であることに注意してください。


sync.Condを使用すると、特定の条件に基づいて goroutine の実行を調整できるため、Go でより制御され同期された並行プログラミングが可能になります。

一般的な使用例

sync.Condは、特定の条件に基づいて goroutine が相互に調整および通信する必要があるシナリオでよく使用されますsync.Condの一般的な使用例を考えてみましょう。

Goroutine 同期

sync.Cond 、複数のゴルーチンの実行を同期するために使用できます。たとえば、処理を進める前に特定の条件が満たされるのを待つ必要があるさまざまなゴルーチンがあるとします。待機中のゴルーチンはcond.Wait()を呼び出し、シグナリング ゴルーチンは条件が満たされたときにcond.Signal()またはcond.Broadcast()を呼び出して待機中のゴルーチンを起動できます。


 package main import ( "fmt" "sync" "time" ) func main() { var wg sync.WaitGroup var mu sync.Mutex cond := sync.NewCond(&mu) wg.Add(2) go func() { fmt.Println("Goroutine 1 is started") defer wg.Done() cond.L.Lock() defer cond.L.Unlock() fmt.Println("Goroutine 1 is waiting for condition") cond.Wait() fmt.Println("Goroutine 1 met the condition") fmt.Println("Goroutine 1 is done") }() go func() { fmt.Println("Goroutine 2 is started") defer wg.Done() time.Sleep(5 * time.Second) // Simulating some work cond.L.Lock() defer cond.L.Unlock() fmt.Println("Goroutine 2 is signaling condition") cond.Signal() fmt.Println("Goroutine 2 completed signaling") fmt.Println("Goroutine 2 is done") }() wg.Wait() }


この例では、2 つの goroutine があります。最初の goroutine はcond.Wait()を使用して条件を待機し、2 番目の goroutine はcond.Signal()を使用して条件を通知します。


プログラムが実行されると、最初のゴルーチンがロックを取得し、 cond.Wait()を呼び出します。条件がまだ満たされていないため、最初のゴルーチンはロックを解放し、実行を中断します。


一方、2 番目のゴルーチンは 5 秒間スリープし、何らかの作業をシミュレートします。ロックを取得してから、 cond.Signal()を呼び出します。待機中のゴルーチンを起動し、ロックを取得して実行します。


sync.Condを使用すると、最初の goroutine は 2 番目の goroutine が条件を通知するまで待機し、2 つの goroutine 間の同期と調整が可能になります。

生産者・消費者問題

sync.Cond 、プロデューサーとコンシューマーという 2 種類のプロセスが共通の固定サイズのバッファまたはキューを共有する、典型的な同期問題であるプロデューサー-コンシューマー問題を解決するのに役立ちます。プロデューサー ゴルーチンは、 cond.Signal()またはcond.Broadcast()を使用して、新しいデータが使用可能になったときにコンシューマー ゴルーチンに通知できます。


 package main import ( "fmt" "sync" "time" ) const MaxMessageChannelSize = 5 func main() { var wg sync.WaitGroup var mu sync.Mutex cond := sync.NewCond(&mu) messageChannel := NewMessageChannel(MaxMessageChannelSize) producer := NewProducer(cond, messageChannel) consumer := NewConsumer(cond, messageChannel) wg.Add(2) go func() { defer wg.Done() for i := range 10 { producer.Produce(fmt.Sprintf("Message %d", i)) } }() go func() { defer wg.Done() for range 10 { consumer.Consume() } }() wg.Wait() } type MessageChannel struct { maxBufferSize int buffer []string } func NewMessageChannel(size int) *MessageChannel { return &MessageChannel{ maxBufferSize: size, buffer: make([]string, 0, size), } } func (mc *MessageChannel) IsEmpty() bool { return len(mc.buffer) == 0 } func (mc *MessageChannel) IsFull() bool { return len(mc.buffer) == mc.maxBufferSize } func (mc *MessageChannel) Add(message string) { mc.buffer = append(mc.buffer, message) } func (mc *MessageChannel) Get() string { message := mc.buffer[0] mc.buffer = mc.buffer[1:] return message } type Producer struct { cond *sync.Cond messageChannel *MessageChannel } func NewProducer(cond *sync.Cond, messageChannel *MessageChannel) *Producer { return &Producer{ cond: cond, messageChannel: messageChannel, } } func (p *Producer) Produce(message string) { time.Sleep(500 * time.Millisecond) // Simulating some work p.cond.L.Lock() defer p.cond.L.Unlock() for p.messageChannel.IsFull() { fmt.Println("Producer is waiting because the message channel is full") p.cond.Wait() } p.messageChannel.Add(message) fmt.Println("Producer produced the message:", message) p.cond.Signal() } type Consumer struct { id int cond *sync.Cond messageChannel *MessageChannel } func NewConsumer(cond *sync.Cond, messageChannel *MessageChannel) *Consumer { return &Consumer{ cond: cond, messageChannel: messageChannel, } } func (c *Consumer) Consume() { time.Sleep(1 * time.Second) // Simulating some work c.cond.L.Lock() defer c.cond.L.Unlock() for c.messageChannel.IsEmpty() { fmt.Println("Consumer is waiting because the message channel is empty") c.cond.Wait() } message := c.messageChannel.Get() fmt.Println("Consumer consumed the message:", message) c.cond.Signal() }


この例では、メッセージを生成してメッセージ チャネルに追加するプロデューサー ゴルーチンと、メッセージを消費するコンシューマー ゴルーチンがあります。メッセージ チャネルの最大サイズは、 MaxMessageChannelSizeによって定義されます。


プロデューサー ゴルーチンはメッセージ チャネルにメッセージを追加し、新しいデータが利用可能になったときにcond.Signal()を使用してコンシューマー ゴルーチンに通知します。メッセージ チャネルがいっぱいの場合、プロデューサー ゴルーチンは、コンシューマーがデータを消費してメッセージ チャネルのスペースを解放するまでcond.Wait()を使用して待機します。


同様に、コンシューマ ゴルーチンはメッセージ チャネルからメッセージを消費し、メッセージ チャネルに空きスペースができたときにcond.Signal()を使用してプロデューサー ゴルーチンに通知します。空の場合、コンシューマ ゴルーチンは、プロデューサーがデータを生成してメッセージ チャネルに追加するまでcond.Wait()を使用して待機します。


ここで、 sync.Cond 、プロデューサーとコンシューマーのゴルーチン間の調整と同期を可能にします。これにより、メッセージ チャネルが空のときにコンシューマーが待機し、メッセージ チャネルがいっぱいのときにプロデューサーが待機することが保証され、プロデューサー - コンシューマー問題が解決されます。

リソースの同期

複数の goroutine が共有リソースへの排他的アクセスを必要とするとします。sync.Cond sync.Cond使用すると、アクセスを調整できます。たとえば、ワーカー goroutine のプールは、処理を開始する前に、一定数のリソースが利用可能になるまで待機する必要がある場合があります。goroutine はcond.Wait()を使用して条件変数を待機し、 cond.Signal()またはcond.Broadcast()を使用してリソースの解放を通知できます。


 package main import ( "fmt" "sync" "time" ) const MaxResources = 3 func main() { var wg sync.WaitGroup var mu sync.Mutex cond := sync.NewCond(&mu) resourceProvider := NewResourceProvider(cond, MaxResources) wg.Add(10) for i := range 10 { go func(workerID int) { defer wg.Done() worker := NewWorker(workerID, cond, resourceProvider) worker.Run() }(i) } wg.Wait() } type ResourceProvider struct { maxResources int availableResources int cond *sync.Cond } func NewResourceProvider(cond *sync.Cond, maxResources int) *ResourceProvider { return &ResourceProvider{ cond: cond, availableResources: maxResources, } } func (rp *ResourceProvider) AvailableResources() int { return rp.availableResources } func (rp *ResourceProvider) AcquireResoirce() { rp.availableResources-- } func (rp *ResourceProvider) ReleaseResource() { rp.availableResources++ } type Worker struct { id int cond *sync.Cond rp *ResourceProvider } func NewWorker(workerID int, cond *sync.Cond, rp *ResourceProvider) *Worker { return &Worker{ id: workerID, cond: cond, rp: rp, } } func (w *Worker) Run() { w.cond.L.Lock() for w.rp.AvailableResources() == 0 { fmt.Printf("Worker %d is waiting for resources\n", w.id) w.cond.Wait() } w.rp.AcquireResoirce() fmt.Printf("Worker %d acquired resource. Remaining resources: %d\n", w.id, w.rp.AvailableResources()) w.cond.L.Unlock() time.Sleep(1 * time.Second) // Simulating work w.cond.L.Lock() defer w.cond.L.Unlock() w.rp.ReleaseResource() fmt.Printf("Worker %d released resource. Remaining resources: %d\n", w.id, w.rp.AvailableResources()) w.cond.Signal() }


この例では、限られたリソースへの排他的アクセスを必要とするワーカー ゴルーチンが複数あります。ワーカー ゴルーチンは、他のワーカーと調整するためにcond.Signal()を使用してリソースを取得および解放します。利用可能なリソースがない場合、ワーカー ゴルーチンは、他のゴルーチンがリソースを解放するまでusing cond.Wait()待機します。


この例では、 sync.Condによってワーカー goroutine 間の同期と調整が可能になり、リソースが利用できない場合にワーカー goroutine が待機することが保証され、リソース アクセスが効果的に同期されます。

イベント通知

sync.Cond 、システム内の特定のイベントや変更について goroutine に通知するために使用できます。たとえば、特定のイベントを待機する goroutine を作成できます。イベントが発生すると、シグナル goroutine はcond.Signal()またはcond.Broadcast()を使用して待機中の goroutine を起動し、イベントを処理できるようにします。


 package main import ( "fmt" "sync" "time" ) const maxWorkersCount = 10 func main() { var counter int32 var wg sync.WaitGroup var mu sync.Mutex cond := sync.NewCond(&mu) wg.Add(maxWorkersCount) for i := range maxWorkersCount { go func(workerID int) { defer wg.Done() fmt.Printf("Worker %d performing work\n", workerID) time.Sleep(1 * time.Second) // Simulate work cond.L.Lock() defer cond.L.Unlock() counter++ if counter == maxWorkersCount { fmt.Println("All workers have reached the barrier") cond.Broadcast() } else { fmt.Printf("Worker %d is waiting at the barrier\n", workerID) cond.Wait() } fmt.Printf("Worker %d passed the barrier\n", workerID) }(i) } wg.Wait() }


ここでは、バリア ポイントで作業を実行し、同期する複数のワーカー ゴルーチンがあります。ワーカー ゴルーチンはカウンターを増分し、バリアに到達したワーカーの数に基づいて、バリアで待機するか、 cond.Wait()cond.Broadcast()を使用してバリアに信号を送ります。


各ワーカー ゴルーチンは、何らかの作業を実行し、ロックを取得してカウンター変数を増分します。現在のワーカーがバリアに到達した最後のワーカーである場合、 cond.Broadcast()を使用してバリア条件をブロードキャストし、待機中のすべてのワーカーを起動します。そうでない場合は、最後のワーカーからの通知を受け取るためにcond.Wait()を使用してバリアで待機します。


バリア同期により、すべてのワーカー ゴルーチンがバリアに到達してから、いずれかがバリアを越えることが保証されます。これは、ワークフローの特定の時点で複数のゴルーチンの実行を同期する必要があるシナリオで役立ちます。


この例では、バリアは単純なカウンターを使用して実装されていることに注意してください。ただし、より複雑なシナリオでは、正しい同期を保証し、競合状態を回避するために、追加の同期メカニズムまたは条件を考慮する必要がある場合があります。

結論

結論として、 sync.Cond Go プログラミング言語の便利な型であり、特定の条件に基づいて goroutine 間の同期と調整を可能にします。条件変数を作成および管理する方法を提供します。条件を待機、シグナル、ブロードキャストするメソッドがあります。`sync.Cond` を使用すると、Go でより制御され同期された並行プログラムを作成できます。


sync.Cond Go 標準ライブラリによって提供される同期プリミティブの 1 つにすぎず、その使用法は並行プログラムの特定の要件によって異なることに注意してください。場合によっては、チャネルやsync.WaitGroupなどの他の同期プリミティブの方が適している場合があります。