ここでいくつかの問題が発生します。スケジューラーの飢餓回避メカニズムは、プロセスを待機している間、タスクがブロックされていると見なします。デッドロックされたスレッドと、プロセスが完了するのを単に待っているスレッドを区別するのは難しいでしょう。その結果、タスクが実行されたり、長時間実行されたりすると、新しいタスクがスケジュールされる場合があります(以下を参照)。ヒルクライムヒューリスティックは、アプリケーションと他のアプリケーションの両方からのシステムの全体的な負荷を考慮に入れる必要があります。実行された作業を最大化しようとするだけなので、システムの全体的なスループットの増加が止まるまで作業が追加され、その後元に戻ります。これがアプリケーションに影響を与えるとは思いませんが、スタベーション回避の問題はおそらく影響します。
これらすべてがMicrosoft®.NETを使用した並列プログラミング、Colin Campbell、Ralph Johnson、Ade Miller、Stephen Toubでどのように機能するかについての詳細を見つけることができます(以前のドラフトはオンラインです)。
「.NETスレッドプールは、プール内のワーカースレッドの数を自動的に管理します。組み込みのヒューリスティックに従ってスレッドを追加および削除します。.NETスレッドプールには、スレッドを挿入するための2つの主要なメカニズムがあります。ワーカーを追加する飢餓回避メカニズムです。キューに入れられたアイテムで進行が見られない場合はスレッド、および可能な限り少ないスレッドを使用しながらスループットを最大化しようとするヒルクライミングヒューリスティック。
飢餓回避の目標は、デッドロックを防ぐことです。この種のデッドロックは、ワーカースレッドが同期イベントを待機しているときに発生する可能性があります。同期イベントは、スレッドプールのグローバルキューまたはローカルキューでまだ保留中のワークアイテムによってのみ満たされます。ワーカースレッドの数が固定されていて、それらのスレッドがすべて同様にブロックされている場合、システムはそれ以上進行できなくなります。新しいワーカースレッドを追加すると、問題が解決します。
山登りヒューリスティックの目標は、スレッドがI / Oまたはプロセッサを停止させるその他の待機条件によってブロックされた場合に、コアの使用率を向上させることです。デフォルトでは、管理対象スレッドプールにはコアごとに1つのワーカースレッドがあります。これらのワーカースレッドの1つがブロックされると、コンピューターの全体的なワークロードによっては、コアが十分に活用されていない可能性があります。スレッドインジェクションロジックは、ブロックされたスレッドと、プロセッサを集中的に使用する長時間の操作を実行しているスレッドを区別しません。したがって、スレッドプールのグローバルキューまたはローカルキューに保留中の作業項目が含まれている場合は常に、実行に長い時間(0.5秒以上)かかるアクティブな作業項目が新しいスレッドプールワーカースレッドの作成をトリガーする可能性があります。
.NETスレッドプールには、作業項目が完了するたびに、または500ミリ秒間隔のいずれか短い方でスレッドを挿入する機会があります。スレッドプールは、この機会を利用して、スレッド数の以前の変更からのフィードバックに基づいて、スレッドを追加(または削除)しようとします。スレッドの追加がスループットに役立つと思われる場合は、スレッドプールがさらに追加します。それ以外の場合は、ワーカースレッドの数が減ります。この手法は、山登りヒューリスティックと呼ばれます。したがって、個々のタスクを短くする理由の1つは「飢餓の検出」を回避することですが、タスクを短くするもう1つの理由は、スレッド数を調整することによってスレッドプールにスループットを向上させる機会を増やすことです。個々のタスクの期間が短いほど、スレッドプールがスループットを測定し、それに応じてスレッド数を調整できる頻度が高くなります。
これを具体的にするために、極端な例を考えてみましょう。500個のプロセッサを集中的に使用する操作を含む複雑な財務シミュレーションがあり、各操作が完了するまでに平均10分かかるとします。これらの操作ごとにグローバルキューにトップレベルのタスクを作成すると、約5分後にスレッドプールが500ワーカースレッドに増加することがわかります。その理由は、スレッドプールがすべてのタスクをブロックされていると見なし、1秒あたり約2スレッドの速度で新しいスレッドの追加を開始するためです。
500ワーカースレッドの何が問題になっていますか?原則として、500コアを使用し、大量のシステムメモリを使用している場合は、何もありません。実際、これは並列コンピューティングの長期的なビジョンです。ただし、コンピューターにそれほど多くのコアがない場合は、多くのスレッドがタイムスライスをめぐって競合している状況にあります。この状況は、プロセッサのオーバーサブスクリプションとして知られています。多くのプロセッサを集中的に使用するスレッドが単一のコアで時間を競うことを許可すると、コンテキストスイッチングのオーバーヘッドが追加され、システム全体のスループットが大幅に低下する可能性があります。メモリが不足していなくても、この状況でのパフォーマンスは、順次計算よりもはるかに悪くなる可能性があります。(各コンテキストスイッチには6,000〜8,000プロセッササイクルかかります。)コンテキストスイッチのコストだけがオーバーヘッドの原因ではありません。の管理対象スレッド。NETは、現在実行中の関数にそのスペースが使用されているかどうかに関係なく、約1メガバイトのスタックスペースを消費します。新しいスレッドを作成するには約200,000CPUサイクル、スレッドをリタイアするには約100,000サイクルかかります。これらは費用のかかる操作です。
タスクがそれぞれ数分もかからない限り、スレッドプールの山登りアルゴリズムは、最終的にスレッドが多すぎることを認識し、それ自体で削減します。ただし、ワーカースレッドを数秒、数分、または数時間占有するタスクがある場合は、スレッドプールのヒューリスティックが破棄されるため、その時点で別の方法を検討する必要があります。
最初のオプションは、アプリケーションをより短いタスクに分解し、スレッドプールがスレッド数を正常に制御して最適なスループットを実現するのに十分な速度で完了することです。2番目の可能性は、スレッドインジェクションを実行しない独自のタスクスケジューラオブジェクトを実装することです。タスクの期間が長い場合は、タスクの実行時間と比較してスケジューリングのコストがごくわずかであるため、高度に最適化されたタスクスケジューラは必要ありません。MSDN®開発者プログラムには、同時実行の最大度を制限する単純なタスクスケジューラの実装例があります。詳細については、この章の最後にある「参考資料」のセクションを参照してください。
最後の手段として、SetMaxThreadsメソッドを使用して、ワーカースレッドの数の上限(通常はコアの数と同じ)でThreadPoolクラスを構成できます(これはEnvironment.ProcessorCountプロパティです)。この上限は、すべてのAppDomainを含むプロセス全体に適用されます。」