これは投稿の抜粋です。投稿全文はこちらからご覧いただけます: https://victoriametrics.com/blog/go-sync-cond/
この投稿は、Go での同時実行性の処理に関するシリーズの一部です:
Go では、sync.Cond は同期プリミティブですが、sync.Mutex や sync.WaitGroup などの兄弟ほど一般的には使用されていません。他の同期メカニズムが代わりに使用される傾向にあるほとんどのプロジェクトや標準ライブラリでさえ、この機能を見かけることはほとんどありません。
そうは言っても、Go エンジニアとしては、sync.Cond を使用するコードを読み進めていて、何が起こっているのかまったく分からないという事態は避けたいでしょう。結局のところ、sync.Cond は標準ライブラリの一部だからです。
したがって、この議論はそのギャップを埋めるのに役立ち、さらに良いことに、それが実際にどのように機能するかをより明確に理解できるようになります。
それでは、sync.Cond とは何なのかを詳しく見ていきましょう。
Goroutine は、共有データの変更など、特定の何かが起こるのを待つ必要がある場合、「ブロック」することができます。つまり、続行の許可が得られるまで作業を一時停止するだけです。これを行う最も基本的な方法は、ループを使用することです。場合によっては、CPU がビジー待機でおかしくなるのを防ぐために time.Sleep を追加することもあります。
これは次のようになります:
// wait until condition is true for !condition { } // or for !condition { time.Sleep(100 * time.Millisecond) }
これは、何も変更されていない場合でもループがバックグラウンドで実行され、CPU サイクルを消費するため、あまり効率的ではありません。
そこで sync.Cond が介入し、ゴルーチンの作業を調整するためのより良い方法になります。専門的に言えば、学歴が高い人にとっては、これは「条件変数」です。
sync.Cond が提供する基本的なインターフェイスは次のとおりです。
// Suspends the calling goroutine until the condition is met func (c *Cond) Wait() {} // Wakes up one waiting goroutine, if there is one func (c *Cond) Signal() {} // Wakes up all waiting goroutines func (c *Cond) Broadcast() {}
それでは、簡単な疑似例を確認してみましょう。今回はポケモンをテーマにしています。特定のポケモンを待っていると想像してください。ポケモンが現れたら他のゴルーチンに通知したいと考えています。
// wait until condition is true for !condition { } // or for !condition { time.Sleep(100 * time.Millisecond) }
この例では、1 つのゴルーチンがピカチュウの出現を待機し、別のゴルーチン (プロデューサー) がリストからランダムにポケモンを選択し、新しいポケモンが出現したときにコンシューマーに信号を送ります。
プロデューサーがシグナルを送信すると、消費者は目を覚まし、正しいポケモンが出現したかどうかを確認します。捕獲されていれば、ポケモンを捕まえます。捕獲されていなければ、消費者はスリープに戻り、次のポケモンを待ちます。
問題は、信号を送信するプロデューサーと実際に目覚める消費者の間にギャップがあることです。それまでの間、コンシューマの goroutine が 1ms よりも遅れて起動する可能性があるため (まれに)、他の goroutine が共有ポケモンを変更するため、ポケモンが変更される可能性があります。したがって、sync.Cond は基本的に次のように言っています: 「おい、何かが変わった!」起きて確認してください。でも遅すぎると、また変わるかもしれません。'
コンシューマーが遅く起きると、ポケモンが逃げ出す可能性があり、ゴルーチンは再びスリープ状態になります。
「うーん、チャネルを使用してポケモンの名前やシグナルを他のゴルーチンに送信できますね。」
その通りです。実際、チャネルはよりシンプルで慣用的で、ほとんどの開発者にとって馴染みがあるため、Go では一般的に sync.Cond よりも好まれます。
上記の場合、チャネルを通じてポケモン名を簡単に送信することも、データを送信せずに空の構造体{}を使用してシグナルを送信することもできます。しかし、私たちが問題としているのは、チャネルを介してメッセージを渡すことだけではなく、共有状態を扱うことです。
この例は非常に単純ですが、複数のゴルーチンが共有ポケモン変数にアクセスしている場合、チャネルを使用すると何が起こるかを見てみましょう:
とはいえ、複数の goroutine が共有データを変更している場合、それを保護するためにミューテックスが依然として必要です。このような場合、適切な同期とデータの安全性を確保するために、チャネルとミューテックスの組み合わせがよく見られます。
「わかりましたが、ブロードキャスト信号はどうですか?」
良い質問です!確かに、単にチャネルを閉じる (close(ch)) だけで、チャネルを使用して待機中のすべてのゴルーチンにブロードキャスト信号を模倣することができます。チャネルを閉じると、そのチャネルから受信しているすべてのゴルーチンに通知が届きます。ただし、閉じたチャネルは再利用できないことに注意してください。一度閉じられると、閉じたままになります。
ところで、実際に Go 2:proposal:sync:remove the Cond type で sync.Cond を削除するという話がありました。
「それで、sync.Cond は何に役立つのですか?」
そうですね、特定のシナリオではチャネルよりも sync.Cond の方が適切な場合があります。
「なぜ sync.Cond にロックが埋め込まれているのですか?」
理論上、sync.Cond のような条件変数は、そのシグナリングが機能するためにロックに結び付けられる必要はありません。
ユーザーが条件変数の外で独自のロックを管理できるようにすることもできます。これにより、より柔軟性が高まるように聞こえるかもしれません。これは実際には技術的な制限ではなく、人的エラーによるものです。
このパターンは直感的ではないため、手動で管理すると間違いが起こりやすくなります。Wait() を呼び出す前にミューテックスのロックを解除し、ゴルーチンが起動したときに再度ロックする必要があります。このプロセスはぎこちなく感じられ、適切なタイミングでロックしたりロックを解除したりするのを忘れるなどのエラーが発生しやすくなります。
しかし、パターンが少しずれているように見えるのはなぜですか?
通常、cond.Wait() を呼び出すゴルーチンは、次のようにループ内の共有状態をチェックする必要があります。
// wait until condition is true for !condition { } // or for !condition { time.Sleep(100 * time.Millisecond) }
sync.Cond に埋め込まれたロックは、ロック/ロック解除プロセスの処理に役立ち、コードがクリーンになり、エラーが発生しにくくなります。このパターンについては後ほど詳しく説明します。
前の例をよく見ると、コンシューマーの一貫したパターンに気づくでしょう。条件を待機 (.Wait()) する前に常にミューテックスをロックし、条件が満たされた後にロックを解除します。
さらに、待機条件をループ内にラップします。ここで復習します:
// Suspends the calling goroutine until the condition is met func (c *Cond) Wait() {} // Wakes up one waiting goroutine, if there is one func (c *Cond) Signal() {} // Wakes up all waiting goroutines func (c *Cond) Broadcast() {}
sync.Cond で Wait() を呼び出すと、現在の goroutine に、何らかの条件が満たされるまで待機するように指示します。
舞台裏で何が起こっているかは次のとおりです:
ここでは、Wait() が内部でどのように動作するかを見ていきます。
// wait until condition is true for !condition { } // or for !condition { time.Sleep(100 * time.Millisecond) }
シンプルではありますが、次の 4 つの主要なポイントを理解できます。
このロック/ロック解除の動作により、よくある間違いを避けるために sync.Cond.Wait() を使用するときに従うことになる典型的なパターンがあります。
// Suspends the calling goroutine until the condition is met func (c *Cond) Wait() {} // Wakes up one waiting goroutine, if there is one func (c *Cond) Signal() {} // Wakes up all waiting goroutines func (c *Cond) Broadcast() {}
「ループを使わずに c.Wait() を直接使用してはどうでしょうか?」
これは投稿の抜粋です。投稿全文はこちらからご覧いただけます: https://victoriametrics.com/blog/go-sync-cond/
以上がGo sync.Cond、最も見落とされている同期メカニズムの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。