次の方法で共有


タスクベースのプログラミング

タスクによるスケーラブルなマルチスレッド プログラミング

Ron Fosner

コンピューターは、プロセッサの速度を上げることから、コア数を増やすことへと進化の方向を変えています。つまり、潜在能力の高いコンピュータが、比較的低コストで手に入るようになります。しかし、このようなシステムの潜在能力を活かすようにプログラミングを行うことが難しくなったとも言えます。こうした複数のプロセッサを余すことなく使用するには、並列処理に踏み込む必要があります。

作業を複数のコアに分散する方法は数多くあります。MSDN マガジンの 10 月号 (msdn.microsoft.com/magazine/gg232758) では、マルチスレッド プログラミングの基本概念をいくつか紹介し、OpenMP とスレッド プールを使用して、コードにスレッド実行を追加する方法について示しました。Visual Studio 2010 のツールを使用して、コアとスレッドの使用率を計測し、スレッドを実装することによってアプリケーションのパフォーマンスがどの程度向上するか確認する方法についても紹介しました。

今月は、タスクベースのプログラミングという、さらに洗練されたマルチスレッド手法について紹介します。タスクを使用することで、使用可能な CPU コアの一部またはすべてにアプリケーションの作業を分散できます。プログラミング方法を少し工夫すると、データどうしの依存関係や時間同期に伴う制約を最小限に抑えたり、時には完全に取り除くこともできます。

前回のコラムを基に、タスクを使用する、より洗練されたマルチスレッド アプリケーションについて紹介します。タスクを使用すれば、アプリケーション自体を使用可能なコア数までスケール変換し、必要な量の作業を実現できます。

迷路をさまようマウス

今月のコラム執筆のため机に向かいながら、並列化するのがやっかいでも、起こっていることが目に見えるような問題はないかと考えました。そこで思いついたのが、2 次元迷路を解く方法です。一見地味に見えますが、実際は、正しく実装するのがかなり困難です。なぜならば、この問題を正しく理解するまで、3 回も試す必要があったからです。

図 1 は、単に直列的に迷路を解決するツールが実行されているところです。この迷路の解決策は、多くの分岐を備え、行き止まりまで続く、単に長く曲がりくねった経路をたどることです。ご想像のとおり、この解決策のアルゴリズムに "マウス" という名前を付けました。マウスは、現在自分がいる区画を確認して、次の区画に進もうとします。壁に突き当たると、左に進もうとします。左に進めなければ、右に行こうとします。左右どちらにも進めなければ、現在の進路は行き止まりであるとマークして、後戻りします。

image: Single-Threaded Maze

図 1 シングルスレッドの迷路

マウスは新しい区画に移動するときに、進まなかった進路を記録します。たとえば、上にも左にも進める場合、その区画と進める方向を記憶します。したがって、マウスは通路を進んでいくごとに、上下左右の入り口を記録し、スタックにプッシュします。マウスが行き止まりに突き当たると、プッシュしておいたいずれかの場所をポップして引き返し、以前に進まなかった方向に進みます。マウスはその徹底した執念深さにより、ゴールにたどり着きます。

後戻りしたマウスが、既に調べた方向には進まないようにする必要があります。ここでは、マウスがある区画に正しく移動できたとき、その区画を "訪問済み" とマークすることで、これを実現しています。つまり、新しい方向に進もうとするマウスは、まず、壁がないことを確認します。壁がなかったら、移動先の区画が "訪問済み" になっていないことを確認し、既に訪問済みであればその区画への移動は取りやめます。図 1 では、これを灰色で塗りつぶすことで表しています。

マウスの解決策

この解決策を目に見えるようにするのは実に簡単なので、迷路を調べるロジックは容易に理解できます。ただ、少しの間見ていると少々眠気を誘うのも事実です。マウスの直列的な動きを簡単なフロー チャートにしたのが図 2 です。

image: Flow Chart of Mouse Actions

図 2 マウスの動きを示すフローチャート

この問題の概念を理解するのは非常に簡単でも、いくつかの要素に制約がないことが事態を難しくします。まず、行き止まりに突き当たるまで、どのくらい長くマウスが動くかわかりません。次に、通路でどのくらい多くの岐路が見つかるかわかりません。

この問題をマルチスレッドで実行しようとすると、面白さが倍増します。この問題をマルチコア対応にするもっとも簡単な方法は、マウスを複数にして、それぞれに固有のスレッドを与えることです。今回採用したのがこの手法です。おまけとして、新しいスレッドに切り替わるたびにアクティブなマウスの色を変えられるため、さらに見た目がわかりやすくなります。

実際には、当初考えていたよりも少し難しくなりました。今回、シングルスレッドのバージョンを完成し、動作するようになってから、そのバージョンをマルチスレッド対応にするという過ちを犯してしまいました。これは、アーキテクチャ上、最大の失敗でした。距離を置いて状況を把握し、アーキテクチャをタスク対応に作り直すまで、3 度の改訂を行いました。

ここでは、こうした改訂作業を詳しく説明しませんが、データをコピーしないこと、メモリの使用を最小限に抑え、さまざまなスレッドから共有データへのアクセスを最適化することで、パフォーマンスを最適化することに集中したとだけ言っておきます。本来、最初の設計では、ロックできるグローバル スタックを用意し、進まなかった分岐を通り過ぎるたびにその場所をスタックにプッシュしていました。スレッドが作業を完了して、処理を必要とするさらなる作業の検索に入ったら、(他のスレッドからの同時アクセスを防ぐため) スタックをロックし、場所と方向をポップしてから、ロックを解除します。これはある程度機能しますが、不恰好なうえに、各マウスにそれぞれ新しいデータを追加して進んだ進路を追跡し、開始位置から現在位置までの進路を認識するようにする必要がありました。

開始位置となるなんらかの部分的な状態を補うためにマルチスレッド プログラムに中間状態の情報を追加したり、特定のキャッシュ動作を追加したり、タスク コードに対して汎用的ではないことを実行したりしている場合は、設計について考え直すときにきています。

結局、各マウスに現在の進路情報を持たせ、この部分をマウスの初期化情報にすることにしました。分岐点にたどり着いたら、マウスのデータ構造を作成して、マウスの現在情報を使ってそのデータ構造を初期化します。たとえば、オリジナルのマウスが右に進むとき、左に進むクローンのマウスを作成します。クローンのマウスには、オリジナルのマウスの記憶を持たせます。各マウスが唯一異なる点は、作成されるマウスの数を追跡するカウンターです。このカウンターにより、マウスに割り当てる色を変えます。

また、方針を変えて、各区画の状態情報を含む迷路のグローバル コピーを 1 つ作成し、状態情報を書き込む際にロックしないようにしました。これは、トレードオフとして受け入れた簡略化で、マウスはそれぞれ独力で進路を進みます。マウスは、常に、ある区画に移動する前にその区画が "訪問済み" とマークされているかどうか確認します。

グローバルな区画データをロックしないため、考えにくいことではありますが、2 匹のマウスがある経路を同時に進み始める可能性はあります。これは、2 匹のマウスがお互い、重複するスタック エントリを使用するか、ループする経路を通ると発生することがあります。どちらの場合でも、マウスが問題なく進路を進み、各スレッドが中断され、再開したときに別のマウスと出くわすことを発見するという事実を受け入れます。この場合、出くわしたマウスが正常に進路を辿っているため、もう一方のマウスはまるで壁にぶつかったように後退します。中断されたマウスは、移動の機会を逃し、なんらかの別の作業を実行します。

区画をマークするのに多くの処理が必要だったとしたら、なんらかの無駄な作業を実行する必要があるという事実を受け入れがたかったと思います。共有データをロックしないようにするために必要なのは、アルゴリズムを少し堅牢にすることだけです。そのような状況に対処できるように設計するということは、失敗する余地が少なくなるということです。多くの場合、マルチスレッド プログラムにおけるエラーの多くは、競合状態、データやカウンターが更新されるタイミングについての仮定など、なんらかのロックの失敗に起因します。

少し古いデータを処理して、そのような状況から復旧できるようなしっかりとしたアルゴリズムを作成できるのであれば、耐久性のあるマルチスレッド アーキテクチャに近づいています。

タスクのキューと依存関係

Windows Vista では、新しいスケジューリング アルゴリズムと新しいプリミティブが導入されています。これらは、Microsoft .NET Framework 4 の多くの機能の基盤となるサポートです。こうした機能の 1 つが、タスク並列ライブラリ (TPL) です。TPL では、fork/join アルゴリズム、parallel_for アルゴリズム、work-stealing アルゴリズム、タスクのインライン アルゴリズムなど、一般的な並列プログラミング アルゴリズムが数多く提供されます。アンマネージ C++ でコーディングしている場合は、Intel の Threading Building Block (TBB) か、Microsoft 並列パターン ライブラリ (PPL) を活用できます。

これらのライブラリには、ジョブとタスク向けにマルチスレッド プログラミングのサポートを提供するクラスがあります。また、スレッド セーフなコンテナー クラスも数多くあります。これらのクラスはテスト済みで、パフォーマンスが最適化されているため、なんらかの理由によりカスタマイズしたバリエーションをどうしても作成しなければならない場合を除き、テスト済みの堅牢なコードを使用することをお勧めします。

今回はスレッドとタスクの初歩を紹介することが目的なので、こうしたスレッド ライブラリのしくみについて調べることは、読者の皆さんにとってメリットがあると考えました。そこで、ThreadPool と SlimReaderWriterLock (SRWLock) という、Windows Vista の 2 つの新機能を含む独自のラッパーを作成しました。これらは、1 つのライターと複数のリーダーがあり、通常、データを長期間ロックしない状況で、データ スレッドを安全にする、コストのかからない方法です。このコラムの目的は、タスクを使用するスレッド プールを実装するに至った過程を理解していただくことです。この場合のタスクは、依存関係を持つことができます。基本的なメカニズムを説明するため、勝手ながら理解しやすいようにコードを変更しました。このコードでも機能しますが、実際に実装する場合は、スレッド ライブラリの中から選択することをお勧めします。

今回の迷路アルゴリズムでは、マルチスレッド アルゴリズムの中でも最も汎用的なものを使用することにしました。つまり、タスクに、(SRWLock を使用して実装される) 依存関係と (OS の ThreadPool を使用する) タスク スケジューラーを持たせることです。これが最も汎用的であるのは、基本的に、タスクは完了させる必要のある単なる作業になり、タスク スケジューラーが OS との通信を行って、タスクをスレッド上で実行させるためです。また、実行する必要があるコードからタスクを作成して、それを ThreadPool に送ることが可能なためです。

課題は、タスクのスケジュールを意味があるように設定するオーバーヘッドを抑えるために、十分な時間を取るタスクを作成することです。実行する必要があるタスクが大きなモノリシックなタスクの場合、スレッドを何個か作成して、それらのスレッド上でコードを実行してもかまいません。一方、多くのアプリケーションには複数のタスクがあり、それらのタスクの実行順序が決まっている場合も、並列に実行できる場合もあります。どのくらいの作業を完了する必要があるのかがあらかじめわかっているときもあれば、たまにしか必要としない拡張した処理をポーリングするだけのとき (特にユーザー入力やなんらかの通信をフェッチしたり反応したりする場合) もあります。これは、ThreadPool と、関連するタスク キューの汎用的な性質によって簡単に処理されます。

ThreadPool のカスタマイズ

ThreadPool の上位にタスク システムを構築する方法を明確に理解するために本当に必要なのは、ThreadPool のインターフェイスのうちの次の 3 つのインターフェイスだけです。

CreateThreadpool();
CloseThreadpool();
TrySubmitThreadpoolCallback();

最初の 2 つは、単なるブックキーピング関数です。TrySubmitThreadpoolCallback は、基本的に、実行する関数へのポインターと、いくつかのコンテキスト変数を受け取ります。実行するタスクを含む TreadPool を読み込むためにこの関数を繰り返し呼び出すと、関数は、先入れ先出し (FIFO) 方式でタスクにサービスを提供します (保証はありません)。

ThreadPool がタスクを操作できるようにするため、ここでは、スレッド プール内のスレッド数をカスタマイズできる、ThreadPool の短いラッパーを作成しました (図 3 参照)。また、タスクに関連付けられたコンテキスト変数を追跡する、submit 関数も作成しました。

図 3 ThreadPool のラッパー

class ThreadPool {
  PTP_POOL m_Pool;
public:
  static VOID CALLBACK WorkCallback(
    PTP_CALLBACK_INSTANCE instance,
    void* Context);
  ThreadPool(void);
  ~ThreadPool(void);
  
  static unsigned GetHardwareThreadsCount();

  // create thread pool that has optimal 
  // number of threads for current hardware
  bool create() { 
    DWORD tc = GetHardwareThreadsCount(); 
    return create(tc,tc); 
  }
  bool create(
    unsigned int minThreads, 
    unsigned int maxThreads = 0);
  void release();
  bool submit(ITask* pWork);
};

興味深いのは、たいていの場合、ハードウェア スレッドと同数しかソフトウェア スレッドを作成しないことです。メイン スレッドに何かをさせる場合、1 つ減らすこともあります。作成するスレッドの数は、タスク キューの大きさとは関係ありません。4 つのスレッドがある ThreadPool を作成して、何百ものタスクを送信してもまったく問題ありません。ただし、1 つの直列ジョブを、何千ものタスクに分割することはお勧めしません。細かいタスクが多すぎることになります。そのような状況になったら、次の 100 個のタスクをスケジュールするタスクを作成するか、タスク ライブラリのいずれかを使用している際は、work-stealing タスクか、インライン タスク、または follow-on タスクを作成します。

今回作成した Task クラス (Task と表記するときは、図 4 のタスク ラッパー クラスを指します) には、他の Task に依存する機能があります。OS の ThreadPool にはこの機能がないため、追加する必要があります。したがって、Task がスレッド上で実行を開始するとき最初に行うことは、未解決の依存関係がないことの確認です。未解決の依存関係があると、タスクを実行するスレッドは、SRWLock を待機することでブロックされます。Task の再スケジュールは、SRWLock が解放されたときにのみ行われます。

図 4 Task ラッパー

class ITask {
protected:
  vector<ITask*>    m_dependentsList;      // Those waiting for me
  ThreadSafe_Int32  m_dependencysRemaining;// Those I'm waiting for

  // The blocking event if we have dependencies waiting on
  SRWLOCK m_SRWLock; 
  CONDITION_VARIABLE m_Dependencies;

  void SignalDependentsImDone();
  ITask(); 
  virtual ~ITask();

public:  
  void blockIfDependenciesArePending();
  void isDependentUpon(ITask* pTask);

  unsigned int queryNumberDependentsRemaining()

  // A parent Task will call this
  void clearOneDependency();
  virtual void doWork(void*) = 0;
  virtual void* context() = 0;
};

さらに、学術目的のアプリケーション以外では見受けられないコードであることをお断りしておきますが、ここにブロックを配置すると、何が起こっているかはっきりとわかります。OS はブロックに気付き、別の Task をスケジュールします。最終的には、プログラミング エラーがない限り、ブロックされたタスクのブロックが解除され、そのタスクの実行が再スケジュールされます。

一般に、即座に中断される Task をスケジュールするのはお勧めしません。ThreadPool のタスク キューは、概して FIFO のため、まず依存関係のないタスクをスケジュールすることを考えます。もしこれを説明目的ではなく、パフォーマンスを最大限に引き出すように作成するとしたら、スレッド プールに依存関係のないタスクを送信するだけのレイヤーを追加していました。ブロックされたスレッドは最終的には入れ替えられるため、これでうまくいきます。どんな場合でも、タスクが完了して SRWLock がこの状況に使えるということを通知するのに、なんらかのスレッド セーフな方法を使用する必要があります。SRWLock を Task クラスと組み込むことは、個々の場合を処理するのに特殊なコードを記述するよりも理にかなっています。

設計上、Task は、Task の依存関係をいくつでも持つことができます。通常、可能であれば待機を減らしたり、取り除いたりすることを希望しますが、Visual Studio のタスク一覧や、Intel の Graphics Performance Analyzers などのツールを使用すれば、待機状態を突き止めることができます。ここで示した実装は、非常に基本的なタスク システムなので、高いパフォーマンスを求めるコードでは使用をお勧めしません。これは、マルチスレッドの入門には適したサンドボックス コードですが、効率の良いコードが必要な場合は、TBB、TPL、PPL を参照してください。

ThreadPool は、次のように、Task のデータ構造をクエリするいくつかのプレフィックス コードを実行する WorkCallback 関数を呼び出します。

VOID CALLBACK ThreadPool::WorkCallback(
  PTP_CALLBACK_INSTANCE instance, void* pTask) {

  ITask * pCurrentTask = (ITask*) pTask;
  pCurrentTask->blockIfDependenciesArePending( );
  pCurrentTask->doWork(pCurrentTask->context() );
  pCurrentTask->SignalDependentsImDone();
}

基本的な操作を次に示します。

  1. ThreadPool が、ThreadPool の内部のタスク キューから WorkCallback を読み込みます。
  2. コードから Task をクエリして、依存関係 (親の依存関係) があるかどうかを確認します。依存関係が見つかったら、実行をブロックします。
  3. 依存関係がなかったら、Task ごとに一意になる Task コードの実際の部分である doWork を呼び出します。
  4. doWork から戻ったら、Task の子の依存関係をすべてクリアします。

ここで重要な点は、ThreadPool クラスには、Task の依存関係を確認してクリアする、前処理コードと後処理コードがあることです。このコードは、各スレッドで実行されますが、関連付けられている一意の Task があります。前処理コードが実行されてから、実際の Task の処理関数が呼び出されます。

タスク クラスのカスタム ラッパーの作成

タスクの基本的な仕事は、なんらかのコンテキストと関数ポインターを提供し、そのタスクがスレッド プールによって実行されるようにすることです。今回は、依存関係を持つことができるタスクを作成したかったたため、依存関係のブロックとブロック解除を追跡するブックキーピングを処理するコードが必要でした (図 4 参照)。

Task を作成するときに依存先の Task へのポインターを提供することで、その Task が別のタスクに依存していることを示すことができます。Task には、待機する必要がある Task の数を示すカウンターと、待機する必要がある Task へのポインターの配列を含めます。

Task に依存関係がなくなったら、そのタスクの機能を行う関数を呼び出します。その機能関数から戻ったら、配列内のすべての Task ポインターをループして、各クラスでその Task の残りの依存関係の数を減らす clearOneDependency を呼び出します。依存関係の数が 0 になると、SRWLock が解放され、それらの依存関係を待機しているタスクを実行したスレッドのブロックが解除されます。タスクを実行しているスレッドのブロックが解除されると、実行が継続されます。

これが、Task クラスと ThreadPool クラスを設計した方法についての基本的な概要です。最終的にこの方法に落ち着いたのは、OS ネイティブのスレッド プールにはこの動作を完全に行うものがなく、読者の皆さんが手を加えることができ、依存関係のメカニズムを制御できるコードを用意したいと考えたためです。最初は、ThreadPool を囲む形で、優先キューを含む、かなり複雑なラッパーを作成していましたが、物事を無駄に複雑にしていること、および必要なのは単なる親子依存関係だけであることに気付きました。スレッド スケジューラーをカスタマイズすることに強い関心をお持ちであれば、Joe Duffy のブログ記事、「Building a Custom Thread Pool (Part 2): A Work Stealing Queue (カスタム スレッド プールを作成する - 第 2 部: ワーク スティーリング キュー)」(tinyurl.com/36k4jcy、英語) を参照してください。

私には、かなり防衛的なコードを記述する傾向があります。また、機能する簡単な実装を記述してから、何も間違っていないことを途中で頻繁に確認しながら、機能をリファクタリングして増加させていくのが好みです。残念ながら、他の変数への参照を受け渡すコードを作成しがちでもあります。参照の受け渡しは慎重に行わないと、マルチスレッド プログラムには望ましくない結果を招きます。

今回は、シングルスレッドの迷路解決策のコードをマルチスレッドに変換するときに、2 回以上行き詰まりを感じました。最終的には、この変数参照の受け渡しを克服するために、値がスレッドで変更される可能性があるときはデータのコピーを渡すようにする必要がありました。

また、慎重を期して、現在のマウスの進路のみを保持するシングルスレッドのバージョンから作成を開始しました。そのため、多くの状態データを追跡しなければならないという問題が生じましたが、既に説明したように、親のデータをすべて保持するクローンを作成することで解決しました。さらに、ThreadPool のラッパーから優先キューを取り除き、グローバルな迷路区画データのロックも取り除くしことにしました。他にもいくつか作業を行いましたが、コードを大幅に簡略化したことで、エラーの原因となりそうな部分も数多く除去しました。

ThreadPool ラッパーと Task クラスは、まさに設計どおりに機能しました。これらのクラスは、いくつかの単体テストで利用して、期待どおりに動作することを確認しました。また、Intel の Graphics Performance Analyzers タスク ツールを使用して、これらのクラスをインストルメント化しました。このツールでは、スレッドに動的にタグを付け、特定のスレッドでコードのどの部分が実行されているかを調べることができます。このようにスレッドの実行を目に見えるようにすることで、期待どおりにスレッドが動作し、ブロックを行い、再スケジュールされていることを確認できました。

マウスを親のクローンとなるようにコードを書き直すと、各マウスが自己完結型になるため、シミュレーションで必要となるブックキーピングを大幅に簡略化することができました。最終的に必要だった唯一の共有データは、区画が訪問済みかどうかを示すグローバルな区画の配列でした。タスクのスケジュールがどのように設定されていくかが目に見えるようにするのが最も重要なことは、いくら強調しても足りません。

マウスのプール

迷路の問題を選んだのは、シングルスレッドのアルゴリズムをマルチスレッドのアルゴリズムに変換することで生じる数多くの問題が明らかになるためです。一番驚いたのは、苦痛に耐えて、管理を試みるブックキーピングの一部を取り除くようにアルゴリズムを書き直したことで、迷路を解くアルゴリズムが突如としてずっと簡単になったことです。実際、シングルスレッドのアルゴリズムよりも単純になりました。これは、分岐点をスタックに保持する必要がなくなったためです。分岐点は、新しいマウスに引き継がれるだけになりました。

設計上、各マウスはその親のクローンなので、マウスはそれぞれ、引き継がれた地点までさかのぼる進路が継承されます。マウスはそれを把握していませんが、わざと、できるだけ遠い進路を選択するよう迷路を生成するアルゴリズムを作成しました。簡単な進路を選択させる意味はありません。テスト プログラムによって、(最終的に行き止まりにたどり着く、ところどころに分岐を持つ長い通路を生成する) 元のアルゴリズムから、非常に分岐が多く通路の短いアルゴリズムまで、多岐にわたる迷路生成アルゴリズムを選択することができます。このようなさまざまな迷路を用意すると、シングルスレッドの解決策とマルチスレッドの解決策の動作の違いがかなり劇的なものになります。

元の迷路アルゴリズムを解決するためにマルチスレッドのメソッドを使用すると、4 CPU システムで、検索時間が 48% も減少しました。これは、アルゴリズムに長い通路がたくさんあり、新たなマウスが生成される機会があまり多くなかったためです (図 5 参照)。

Figure 5 Solving the Original Long-Maze Algorithm

図 5 元の長い迷路のアルゴリズムの解決

図 6 は、これよりも分岐が多い迷路です。この迷路では、マウスが新たに生み出され、同時に検索を実行させる機会が増えました。図 6 で示す通路が短い迷路でマルチスレッドの解決策を実行すると、多くのタスクを使用することで、探索時間が 95% 減少しました。

Figure 6 Multiple Mice Make It Easy to Solve the Maze in Much Less Time

図 6 複数のマウスが短時間で迷路を探索する例

このことから、分割による影響を特に受けやすい問題があることがわかります。重要なのは、この迷路プログラムは見た目におもしろくなるように設計されている点です。つまり、マウスの進み具合と、その道筋を見ることができます。今回の目的が、最短時間で迷路を探索することだけだったら、マウスを表示しませんでしたが、それでは見ていてあまりおもしろくありません。

緩んだ糸 (スレッド)

アプリケーションの実行速度を上げようとする開発者をサポートするときに見かける一番大きな問題は、マルチスレッド化することへのためらいでした。ためらう気持ちは理解できます。マルチスレッドを追加するということは、ほとんどのプログラマになじみのない複雑なレイヤーを、あまり経験のない分野に突如追加するということです。

残念ながら、マルチスレッドを敬遠している限り、まだ利用していないコンピューターの処理能力の優れた部分は置き去りにされたままです。

今回の記事では、タスク システムの基本的なしくみを示して、大きなジョブをタスクに分割する方法の基本について説明しました。ただし、ここで紹介した手法は優れたものですが、今も、これからもマルチコア ハードウェアのパフォーマンスを最大限に引き出すベスト プラクティスではありません。アプリケーションを実行するハードウェアでさらにパフォーマンスを向上させることに興味があれば、この点に留意してアプリケーションを設計する必要があります。

アプリケーションのパフォーマンスを最大限に引き出し、かつスケーラビリティを高める最適な方法は、既存の並列処理ライブラリのいずれかを利用して、アプリケーションのニーズをこれらのライブラリが提供するさまざまなアーキテクチャと一致させることです。アンマネージ アプリケーション、リアルタイム アプリケーション、またはパフォーマンスが非常に重要なアプリケーションは、通常、TBB で提供されるインターフェイスのいずれかを使用すると最適に機能します。他方、マネージ アプリケーションには、.NET Framework 4 のさまざまなマルチスレッドのオプションがあります。どの場合でも、どのスレッド API を選択するかによって、アプリケーションの全体構造と、タスクが共存機能するように設計する方法は異なります。

今後の記事では、これらの手法を利用する実際の実装について紹介し、スレッド ライブラリを使用するために独自の実装を設計できるように、こうしたさまざまなスレッド ライブラリでアプリケーションを構築する方法を示します。

いずれにしても、これで基本的なスレッド手法のいくつかを試してみる基礎知識を習得したことになるため、スレッド ライブラリを参照して、これから作成するアプリケーションをどのように設計するのが最適か、考え始めてください。マルチスレッド化は難しいことですが、これらの拡張可能なライブラリのいずれかを使用することは、今も、これからも、ハードウェアのパフォーマンスを最大限に引き出すことへの第一歩になります。

Ron Fosner は、何年にもわたり高パフォーマンスのアプリケーションとゲームを Windows で最適化してきて、そのこつを掴んできたところです。彼は Intel でグラフィックと最適化の専門家として活躍しており、あらゆる CPU コアが完全に実行されているのを見ると幸せな気持ちになります。連絡先は Ron@directx.com (英語のみ) です。

この記事のレビューに協力してくれた技術スタッフの Aaron Coday、Orion Granatir、および Brad Werth に心より感謝いたします。