6

シングル プロデューサー スレッド、シングル コンシューマー スレッドのロックレス キューがあり、プロデューサーが長期間にわたってデータを生成しない場合があるとします。キューに何もないときにコンシューマー スレッドをスリープ状態にすることは有益です (電力を節約し、他のプロセス/スレッドのために CPU を解放するため)。キューがロックレスではない場合、この問題を解決する簡単な方法は、生成スレッドにミューテックスをロックさせ、その作業を実行させ、条件変数を通知してロックを解除し、読み取りスレッドがミューテックスをロックするまで条件変数を待機することです。 、その読み取りを行い、ロックを解除します。しかし、ロックレス キューを使用している場合、まったく同じ方法でミューテックスを使用すると、そもそもロックレス キューを使用することで得られるパフォーマンスが失われてしまいます。

単純な解決策は、キューへの挿入ごとにプロデューサーにミューテックスをロックさせ、条件変数を通知してからミューテックスのロックを解除し、実際の作業 (キューへの挿入) を完全にロックの外に保ち、コンシューマーに実行させることです。同じように、ミューテックスをロックし、条件変数を待機し、ロックを解除し、キューからすべてを取り出してから繰り返し、キューの読み取りをロックの外に保ちます。ただし、ここには競合状態があります。リーダーがキューから離れてからスリープ状態になるまでの間に、プロデューサーがアイテムをキューに挿入した可能性があります。これでリーダーはスリープ状態になり、プロデューサーが別のアイテムを挿入して条件変数に再度シグナルを送るまで、無期限にスリープ状態になる可能性があります。これは、特定のアイテムがキューを通過するのに非常に長い時間がかかるように見える場合があることを意味します. キューが常にアクティブである場合、これは問題にならない可能性がありますが、常にアクティブである場合は、条件変数を完全に忘れてしまう可能性があります。

AFAICT解決策は、プロデューサーが通常のニーズロックキューを使用しているかのように動作することです。ミューテックスをロックし、ロックレス キューに挿入し、条件変数を通知し、ロックを解除する必要があります。ただし、消費者は異なる行動を取る必要があります。起動すると、キューが読み取られるまで待機するのではなく、すぐにミューテックスのロックを解除する必要があります。次に、できる限り多くのキューをプルして処理する必要があります。最後に、消費者がスリープ状態になることを考えている場合にのみ、ミューテックスをロックし、データがあるかどうかを確認し、データがある場合はロックを解除して処理するか、そうでない場合は条件変数を待機します。この方法では、ロックフル キューよりもミューテックスの競合が少なくなりますが、キューにデータが残ったままスリープ状態になるリスクはありません。

これが最善の方法ですか?代替手段はありますか?

: 「最速」とは、実際には「キューを何度もチェックするためにコアを専用にすることなく最速」を意味しますが、それはタイトルには当てはまりません;p

1 つの代替方法: 単純なソリューションを使用しますが、キューを通過するアイテムに対して許容できる最大待機時間に対応するタイムアウトを使用して、コンシューマーを条件変数で待機させます。ただし、必要なタイムアウトがかなり短い場合は、OS の最小待機時間を下回っているか、CPU の消費量が多すぎる可能性があります。

4

1 に答える 1

1

Dr Dobbsの記事(または同様のもの)のロックフリーの単一プロデューサー単一コンシューマーキューを使用していると想定しているので、そこから用語を使用します。

その場合、「AFAICT」で始まる段落で提案された答えは良いですが、少し最適化できると思います。

  • コンシューマーでは-あなたが言うように、コンシューマーがキューを使い果たしてスリープを検討しているとき(そしてその時だけ)、ミューテックスをロックし、キューを再度チェックしてから、
    • キューに新しいアイテムがあった場合は、ミューテックスを解放して作業を続行します
    • または条件変数をブロックします(当然、空でないキューを見つけるためにウェイクアップしたときにミューテックスを解放します)。
  • プロデューサーで:
    • 最初にのコピーを取り、lastそれを呼び出しますsaved_last
    • new_item通常どおりアイテムを追加してから、dividerポインタのコピーを取り、それを呼び出しますsaved_divider
    • の値が、挿入したばかりのオブジェクトsaved_dividerに等しい場合new_item、オブジェクトはすでに消費されており、作業は完了です。
    • それ以外の場合、の値がにsaved_divider等しくない場合はsaved_last、コンシューマーをウェイクアップする必要はありません。それの訳は:
      • 新しいオブジェクトを追加した直後に、それdividerがまだnew_itemまたはsaved_last
      • 挿入を開始してから、lastこれら2つの値しかありません
      • divider消費者は、が等しい場合にのみ停止しますlast
      • したがって、消費者はまだ起きていなければならず、寝る前にあなたの新しいアイテムに到達します。
    • それ以外の場合は、ミューテックスをロックし、condvarに信号を送ってから、ミューテックスを解放します。(ここでミューテックスを取得すると、コンシューマーがキューが空であることに気づき、実際にcondvarでブロックするまでの間にコンダーにシグナルを送信しないようになります。)

これにより、コンシューマーがビジー状態のままになる傾向がある場合に、コンシューマーがまだ起きている(そして眠ろうとしていない)ことがわかっているときにミューテックスをロックすることを回避できます。また、ミューテックスが保持される時間を最小限に抑え、競合の可能性をさらに減らします。

上記の説明は非常に言葉が多いですが(アルゴリズムが何であるかだけでなく、なぜそれが機能するのかについての説明を含めたかったので)、それから得られるコードは非常に単純でなければなりません。

もちろん、それが実際に行う価値があるかどうかは多くのことに依存します。パフォーマンスがあなたにとって重要であるかどうかを測定することをお勧めします。mutex / condvarプリミティブの優れた実装は、ほとんどの場合、内部でアトミック操作を使用するため、通常、必要な場合、つまりブロックする必要がある場合、または確実にいくつかある場合にのみ、カーネル呼び出し(最も高価なビット!)を行います。スレッドが待機している-したがって、mutex関数を呼び出さないことで節約できる時間は、ライブラリ呼び出しのオーバーヘッドにすぎない可能性があります。

于 2010-12-09T21:41:16.963 に答える