3

要件は次のとおりです。処理するアイテムはグローバル キューに格納されます。いくつかのハンドラ スレッドは、グローバル キューからアイテムを取得して処理します。プロデューサー スレッドはアイテムを継続的かつ迅速にグローバル キューに追加します(すべてのディーラー スレッドの処理速度よりもはるかに高速です。また、ハンドラー スレッドは計算集約型です。最高のパフォーマンスは、CPU を完全に使用することです)。そのため、もう 1 つのcountKeeping スレッドを使用して、 BOTTOMからTOPまでのように、キューの長さを特定の範囲に維持します (メモリを使いすぎないようにするため)。

ManualResetEvent「キューに追加可能」ステータスの変更に対処するために使用します。グローバルキューは

Queue<Object> mQueue = new Queue<Object>;
ManualResetEvent waitingKeeper = new ManualResetEvent(false);  

ハンドラスレッド

void Handle()
{
    while(true)
    {
        Object item;
        lock(mQueue)
        {
            if(mQueue.Count > 0)
                item = mQueue.Dequeue();
        }
        // deal with item, compute-intensive
    }
}

プロデューサ スレッドは AddToQueue() 関数を呼び出してアイテムを mQueue に追加します。

void AddToQueue(Object item)
{
    waitingKeeper.WaitOne();
    lock(mQueue)
    {
        mQueue.Enqueue(item);
    }
}

countKeeping スレッドは主に次のようなものです

void KeepQueueingCount()
{
    while(true)
    {
        // does not use 'lock(mQueue)'
        // because I don't need that specific count of the queue
        // I just need the queue does not cost too much memory
        if(mQueue.Count < BOTTOM)
            waitingKeeper.Set();
        else if(mQueue.Count > TOP)
            waitingKeeper.Reset();
        Thread.Sleep(1000);
    }
}

ここで問題が発生します。
BOTTOM と TOP を小さい数値に設定すると、BOTTOM = 20、TOP = 100 のように、クアッド コア CPU ではうまく動作しますが (CPU 使用率が高くなります)、シングル CPU ではうまく動作しません (CPU 使用率は比較的大きく変動します。 )。
BOTTOM と TOP をより大きな数値 (BOTTOM = 100、TOP = 300) に設定すると、シングル CPU ではうまく機能しますが、クアッド コア CPU ではうまく機能しません。
どちらの環境でも、どちらの条件でも、メモリはあまり使用されていません (せいぜい 50M 程度)。

論理的には、BOTTOM と TOP を大きくすると、(メモリがあまり使用されていない場合) パフォーマンスが向上します。これは、ハンドラー スレッドが処理する項目が増えるためです。しかし、事実はそうではないようです。

問題の原因を見つけるために、いくつかの方法を試しました。lock(mQueue)そして、スレッドを維持するために使用すると、上記の2つのCPU条件で両方ともうまく機能することがわかりました。
新しいcountKeepingスレッドは主にこのようなものです

void KeepQueueingCount()
{
    bool canAdd = false;
    while(true)
    {
        lock(mQueue)
        {
            if(mQueue.Count < BOTTOM)
                canAdd = true;
            else if(mQueue.Count > TOP)
                canAdd = false;
        }
        if(canAdd)
            waitingKeeper.Set();
        else
            waitingKeeper.Reset();
        // I also did some change here
        // when the 'can add' status changed, will sleep longer
        // if not 'can add' status not changed, will sleep lesser
        // but this is not the main reason
        Thread.Sleep(1000);
    }
}

だから私の質問は

  1. countKeeping threadlockで使用しなかった場合、グローバル キューの範囲がさまざまな CPU 条件でパフォーマンスに影響するのはなぜですか (ここでは、パフォーマンスは主に CPU 使用率です)。
  2. countKeeping threadで使用するlockと、さまざまな条件でパフォーマンスが向上します。何がこれに実際に影響しますか?lock
  3. を使用する代わりに「追加可能」ステータスを変更するより良い方法はあります ManualResetEventか?
  4. 私の要件に合ったより良いモデルはありますか?または、プロデューサースレッドが継続的かつ迅速に動作 するときに、メモリがあまり使用されないようにするより良い方法はありますか?

---UPDATE---
Producer threadの主要部分は以下の通りです。STEPは、データベースからの各クエリのアイテム数です。クエリは、すべてのアイテムがクエリされるまで、順番に行われます。

void Produce()
{
    while(true)
    {
        // query STEP items from database
        itemList = QuerySTEPFromDB();
        if(itemList.Count == 0)
            // stop all handler thread
            // here, will wait for handler threads handle all items in queue
            // then, all handler threads exit
        else
            foreach(var item in itemList)
                AddToQueue(item);
    }
}
4

1 に答える 1

4

同時キューの例は、競合が非常に多いがロックに費やされる時間がほとんどない場合 (キューとデキューの時間だけ)、アトミックな比較とスワップのスピンロックがかなりうまくいく傾向がある典型的な例です。

https://msdn.microsoft.com/en-us/library/dd460716%28v=vs.110%29.aspx

また、.NET には、その種のアトミック CAS スピンロック設計を使用する並行キューが既に用意されていることにも注意してください。

短時間しか使用されない非常に競合性の高い共有リソースがある場合、高レベルのロックは非常に高価になります。

大雑把な視覚的アナロジーを使用する場合 (誇張された、人間レベルの時間単位を使用)、店があり、行列ができていると想像してください。しかし、店員は仕事がとても速く、列は毎秒動いています。

ここでクリティカル セクション/ミューテックスを使用すると、すべての顧客がまだ自分の番ではないことに気付いたときに居眠りをして昼寝をするようなものになります。それから彼らの番になったら、誰かが彼らを起こさなければなりません: - 「おい、今度はお前の番だ! 起きろ!」- 「え、え? オーケー」ご想像のとおり、スレッドのブロック/一時停止に時間がかかるため、順番を待っているスレッドとの列がますます大きくなる傾向があります。

これが、CPU 使用率の変動が見られる理由でもあります。スレッドは、ロックの周りでトラフィック ジャムを形成し、一時停止/スリープ状態になる可能性があり、スレッドがスリープして順番を待っている間、CPU 使用率が低下します。マルチスレッドは完全に事前に定義された順序でコードを実行するとは限らないため、これもかなり不確定です。そのため、スレッドがロックの周りでトラフィック ジャムを形成できるように設計されている場合、スパイクが発生する可能性があります。あるセッションでは幸運にも、このような時間に敏感なケースで高速なパフォーマンスが得られたとしても、別のセッションでは不運に見舞われ、パフォーマンスが非常に低下する可能性があります。最悪のシナリオでは、

ここで低レベルのスピンロックを使用すると、列ができたときに顧客が居眠りをしないようになります。彼らはただ立って待っているだけで、非常に焦り、常に自分の番かどうかを確認しています。ラインが非常に速く動いている場合、これははるかにうまく機能します。

あなたの問題は、生産者が消費者が消費するよりもはるかに速く生産するという点で、やや珍しいものです。ここでは、一度にどれだけ生産できるかという上限の考え方は理にかなっているように見えますが、そのように処理を絞ることができます。また、別のカウントキーパースレッドでそれを解決する理由もわかりません(その部分がよくわかりませんでした)。キューが小さくなるまで上限に達した場合、プロデューサーがアイテムをキューに入れないようにすることができると思います。

メモリのスパムを避けるためにその上限を維持したいかもしれませんが、(適切なロックを使用した後)プロデューサーをスリープまたは譲り渡して処理分散のバランスを取り、プロデューサーがエンキューするたびに消費者に向けてより偏らせる方がよいと思います処理するアイテム。このようにして、生産者がその制限に達したときに生産者を妨害することはありません。代わりに、消費者が生産者に大きく遅れをとらない速度で消費する機会を得るために、その制限に達するのを避けることがポイントです。

于 2015-05-19T04:35:39.050 に答える