4

C# で物理オブジェクトの処理を高速化するために、線形更新アルゴリズムを並列アルゴリズムに変更することにしました。ジョブのキューを完了するために構築された ThreadPool を使用するのが最善の方法だと思いました。

並列アルゴリズムを最初に実装したとき、すべての物理オブジェクトのジョブをキューに入れました。1 つのジョブはかなり短時間で完了することに注意してください (力、速度、位置を更新し、周囲のオブジェクトの古い状態との衝突をチェックしてスレッド セーフにするなど)。次に、1 つの待機ハンドルを使用して、すべてのジョブが終了するのを待ちます。物理オブジェクトが完了するたびに、インターロックされた整数をデクリメントします (ゼロになると、待機ハンドルを設定します)。次に行う必要があるタスクは、オブジェクトをすべて更新する必要があるため、待機が必要でした。

私が最初に気づいたのは、パフォーマンスがクレイジーだったことです。平均すると、スレッド プーリングは少し速くなったように見えましたが、パフォーマンスに大きなスパイクがありました (更新ごとに 10 ミリ秒のオーダーで、ランダムに 40 ~ 60 ミリ秒にジャンプしました)。ANTS を使用してこれをプロファイリングしようとしましたが、スパイクが発生した理由についての洞察を得ることができませんでした。

私の次のアプローチは、引き続き ThreadPool を使用することでしたが、代わりにすべてのオブジェクトをグループに分割しました。私は最初、8 つのグループだけから始めました。これは、コンピューターのコアがすべて同じだったからです。パフォーマンスは素晴らしかった。シングルスレッドのアプローチよりもはるかに優れており、スパイクはありませんでした (更新ごとに約 6 ミリ秒)。

私が考えた唯一のことは、1 つのジョブが他のジョブより先に完了した場合、アイドル状態のコアが存在するということでした。そのため、ジョブの数を約 20、さらには最大 500 に増やしました。予想どおり、5ms まで低下しました。

だから私の質問は次のとおりです:

  • ジョブ サイズを高速または多にするとスパイクが発生するのはなぜですか?
  • ThreadPool の最適な使用方法を理解するのに役立つ、ThreadPool の実装方法に関する洞察はありますか?
4

5 に答える 5

3

ご想像のとおり、スパイクは、スレッド プールを管理し、タスクをそれらに分散するコードによって引き起こされる可能性があります。

並列プログラミングの場合、(スレッドプールを使用している場合でも) 異なるスレッド間で作業を「手動で」分散するよりも洗練されたアプローチがあります。

概要とさまざまなオプションについては、たとえば、.NET Framework での並列プログラミングを参照してください。あなたの場合、「解決策」は次のように簡単です。

Parallel.ForEach(physicObjects, physicObject => Process(physicObject));
于 2012-09-08T19:31:07.327 に答える
2

あなたの2つの質問に対する私の見解は次のとおりです。

質問 2 (スレッド プールのしくみ) から始めたいと思います。これは、質問 1 に答える鍵を実際に保持しているためです。ワーカー スレッドの数 (必要に応じて縮小または拡大する場合があります)。ユーザーが呼び出すとQueueUserWorkItem、タスクが作業キューに入れられます。ワーカーはキューをポーリングし続け、アイドル状態の場合は作業を行います。タスクを引き受けると、それを実行し、さらに作業を行うためにキューに戻ります (これは非常に重要です!)。したがって、作業はワーカーによってオンデマンドで行われます。ワーカーがアイドル状態になると、より多くの作業が必要になります。

上記のことから、質問 1 に対する答えを理解するのは簡単です (なぜ、より細粒度のタスクでパフォーマンスの違いが見られたのですか): 細粒度では、より多くの負荷分散が得られるためです(非常に望ましい特性)。つまり、ワーカーは多かれ少なかれ同じ量の作業を行い、すべてのコアが均一に活用されます。あなたが言ったように、粗粒度のタスク分散では、より長いタスクとより短いタスクが存在する可能性があるため、1 つまたは複数のコアが遅れて、全体的な計算が遅くなる可能性がありますが、他のコアは何もしません。小さなタスクでは、問題はなくなります。各ワーカー スレッドは、一度に 1 つの小さなタスクを処理し、その後戻ってさらに多くのタスクを処理します。1 つのスレッドが短いタスクを選択すると、より頻繁にキューに移動し、より長いタスクを実行すると、キューに移動する頻度が低くなるため、バランスが取れています。.

最後に、ジョブの粒度が細かすぎる場合、プールが 1,000 を超えるスレッドに拡大する可能性があることを考慮すると、すべてのスレッドがより多くの作業を行うために戻ったときにキューで非常に高い競合が発生します (これは非常に頻繁に発生します)。あなたが見ているスパイクのために。基礎となる実装がブロッキング ロックを使用してキューにアクセスする場合、コンテキスト スイッチが非常に頻繁に発生し、パフォーマンスが大幅に低下し、かなりランダムに見えます。

于 2012-09-09T07:06:18.113 に答える
0

ThreadPoolの詳細については、ここから開始してくださいThreadPoolクラス

.NET Frameworkの各バージョンでは、ThreadPoolを間接的に利用する機能がますます追加されています。たとえば、前述のParallel.ForEachメソッドは、コードをより読みやすく、すっきりさせるSystem.Threading.Tasksとともに.NET4で追加されました。これについて詳しくは、タスクスケジューラもご覧ください。

非常に基本的なレベルでは、それが行うことは次のとおりです。たとえば、20個のスレッドを作成し、それらを点灯させます。非同期を実行するためのデリゲートを受信するたびに、リストからアイドルスレッドを取得してデリゲートを実行します。使用可能なスレッドが見つからない場合は、キューに入れます。deletegateの実行が完了するたびに、キューにアイテムがあるかどうかがチェックされ、ある場合は1つがピークになり、同じスレッドで実行されます。

于 2012-09-08T20:31:06.020 に答える
0

質問 1 の答え: これはスレッドの切り替えによるものです。スレッドの切り替え (または OS の概念ではコンテキストの切り替え) は、各スレッドを切り替えるのにかかる CPU クロックです。ほとんどの場合、マルチスレッドはプログラムとプロセスの速度を向上させますが、それがプロセスの場合非常に小さくて速いサイズの場合、コンテキストの切り替えにスレッドの自己プロセスよりも時間がかかるため、プログラム全体のスループットが低下します。これに関する詳細については、OS の概念の本 を参照してください。

質問 2 の答え: 実際、私は ThreadPool の全体的な洞察を持っていますが、その構造を正確に説明することはできません。

于 2012-09-08T19:34:50.480 に答える