Go sync.Cond、最も見落とされている同期メカニズム

DDD
リリース: 2024-10-30 06:43:28
オリジナル
299 人が閲覧しました

これは投稿の抜粋です。投稿全文はこちらからご覧いただけます: https://victoriametrics.com/blog/go-sync-cond/

この投稿は、Go での同時実行性の処理に関するシリーズの一部です:

  • 同期に進みます。Mutex: 通常モードと飢餓モード
  • Go sync.WaitGroup と調整の問題
  • 同期プールとその背後にある仕組み
  • 最も見落とされている同期メカニズムである sync.Cond を実行します (ここにあります)
  • Go sync.Map: 適切な仕事に適したツール
  • Go Singleflight は DB ではなくコードに溶け込みます

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 が介入し、ゴルーチンの作業を調整するためのより良い方法になります。専門的に言えば、学歴が高い人にとっては、これは「条件変数」です。

  • 1 つのゴルーチンが何かが起こるのを待っているとき (特定の条件が true になるのを待っているとき)、Wait() を呼び出すことができます。
  • 別の goroutine は、条件が満たされる可能性があることが分かると、Signal() または Broadcast() を呼び出して待機中の goroutine を起動し、次に進む時期が来たことを知らせることができます。

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() {}
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

Go sync.Cond, the Most Overlooked Sync Mechanism

sync.Cond の概要

それでは、簡単な疑似例を確認してみましょう。今回はポケモンをテーマにしています。特定のポケモンを待っていると想像してください。ポケモンが現れたら他のゴルーチンに通知したいと考えています。

// 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 の方が適切な場合があります。

  1. チャネルを使用すると、値を送信して 1 つのゴルーチンにシグナルを送信したり、チャネルを閉じてすべてのゴルーチンに通知したりできますが、両方を行うことはできません。 sync.Cond を使用すると、よりきめ細かい制御が可能になります。 Signal() を呼び出して単一の goroutine を起動するか、Broadcast() を呼び出してすべての goroutine を起動できます。
  2. また、Broadcast() は必要なだけ何度でも呼び出すことができますが、チャンネルが閉じられると実行できなくなります (閉じられたチャンネルを閉じるとパニックが引き起こされます)。
  3. チャネルには、共有データを保護するための組み込みの方法が用意されていません。ミューテックスを使用して個別に管理する必要があります。一方、sync.Cond は、ロックとシグナリングを 1 つのパッケージに組み合わせることで、より統合されたアプローチを提供します (そしてパフォーマンスも向上します)。

「なぜ 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() {}
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

Cond.Wait()

sync.Cond で Wait() を呼び出すと、現在の goroutine に、何らかの条件が満たされるまで待機するように指示します。

舞台裏で何が起こっているかは次のとおりです:

  1. ゴルーチンは、同じ条件で待機している他のゴルーチンのリストに追加されます。これらのゴルーチンはすべてブロックされています。つまり、Signal() または Broadcast() 呼び出しによって「ウェイクアップ」されるまで続行できません。
  2. ここで重要なのは、Wait() を呼び出す前にミューテックスをロックする必要があるということです。Wait() は重要なことを行うため、ゴルーチンをスリープさせる前に自動的にロックを解放します (Unlock() を呼び出します)。これにより、元の goroutine が待機している間に、他の goroutine がロックを取得して作業を実行できるようになります。
  3. 待機中のゴルーチンが (Signal() または Broadcast() によって) 起動されても、すぐには作業を再開しません。まず、ロック (Lock()) を再取得する必要があります。

Go sync.Cond, the Most Overlooked Sync Mechanism

sync.Cond.Wait() メソッド

ここでは、Wait() が内部でどのように動作するかを見ていきます。

// wait until condition is true
for !condition {  
}

// or 
for !condition {
    time.Sleep(100 * time.Millisecond)
}
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

シンプルではありますが、次の 4 つの主要なポイントを理解できます。

  1. Cond インスタンスのコピーを防ぐチェッカーがあります。コピーするとパニックになります。
  2. cond.Wait() を呼び出すとすぐにミューテックスのロックが解除されるため、cond.Wait() を呼び出す前にミューテックスをロックする必要があります。そうしないとパニックが発生します。
  3. ウェイクアップされた後、cond.Wait() はミューテックスを再ロックします。つまり、共有データの使用が完了した後に再度ロックを解除する必要があります。
  4. sync.Cond の機能のほとんどは、通知にチケットベースのシステムを使用する、notifyList と呼ばれる内部データ構造を使用して Go ランタイムに実装されています。

このロック/ロック解除の動作により、よくある間違いを避けるために 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() {}
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

Go sync.Cond, the Most Overlooked Sync Mechanism

sync.Cond.Wait() を使用する一般的なパターン

「ループを使わずに c.Wait() を直接使用してはどうでしょうか?」


これは投稿の抜粋です。投稿全文はこちらからご覧いただけます: https://victoriametrics.com/blog/go-sync-cond/

以上がGo sync.Cond、最も見落とされている同期メカニズムの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:dev.to
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート