タスク・スケジューリングの動作

スケジューラーは、タスクグラフ を評価します。 グラフは、ノードがタスクを表す有向グラフで、 それぞれ を指しています。親は、自身の完了を待っている別のタスクか、NULL のいずれかです。 task::parent() メソッドは、親ポインターの読み取り専用アクセスをプログラマーに提供します。 各タスクには、そのタスクを親として持つタスク数をカウントする refcount があります。 グラフは、時間とともに進化します。

フィボナッチ例のタスクグラフ

上記の図は、フィボナッチ例のタスクグラフを示しています。

スケジューラーは、メモリー容量とスレッド間通信の両方を最小限に抑える方法でタスクを実行します。 このためには、深さ優先の実行と幅優先の実行のバランスをとる必要があります。 ツリーが有限であると仮定すると、シリアル実行には深さ優先が最も良い方法です。これには、次のような理由があります。

幅優先の実行はメモリーを消費するという問題がありますが、物理スレッドの数が無限にある場合は、並列処理を最大限に引き出す実行方法です。 しかし、物理スレッドは限られているため、利用可能なプロセッサーを常に稼動状態にできる分だけ幅優先の実行を使用すると良いでしょう。

スケジューラーは、深さ優先の実行と幅優先の実行を組み合わせて実装します。 各スレッドには、実行する準備ができているタスクのデック[8] があります。 スレッドがタスクを生成すると、自身のデックの下部にタスクをプッシュします。次の図は、上記のタスクグラフに対応するスレッドのデックを示しています。

スレッドのデック

スレッドがタスクグラフ評価に入ると、スレッドは次の規則のうち一番最初に当てはまる規則で取得されたタスクを続けて実行します。

  1. 前のタスクの execute メソッドによって返されたタスクを使用する。 この規則は、executeNULL を返した場合には適用されません。

  2. 自身の デックの下部 からタスクをポップする。 この規則は、デックが空の場合は適用されません。

  3. 無作為に選択された別の デックの上部 からタスクをスチールする。 選択されたデックが空の場合、スレッドはこの規則が当てはまるまで実行します。

規則 1 は、「スケジューラーのバイパス」で説明されています。 規則 2 の全体への影響は、スレッドに生成された最も若い タスクを実行することです。スレッドがタスクを実行できなくなるまで、深さ優先の実行が行われます。 その後、規則 3 が適用されます。スレッドは別のスレッドで生成された最も古い タスクをスチールし、潜在的な並列処理を実際の並列処理に変換する幅優先の実行が一時的に行われます。

タスクの取得は常に自動で、タスクグラフ評価の一部として行われます。 タスクのデックへの格納は、明示的または暗黙的です。スレッドは常に、タスクを自身のデックの下部にプッシュします。別のスレッドのデックには決してプッシュしません。 あるスレッドで生成したタスクを別のスレッドに転送できるのはスチールだけです。

スレッドがタスクを自身のデックにプッシュする 3 つの条件があります。

「フィボナッチ数」の例では、子の完了を明示的に待っているため、暗黙的なプッシュは行われません。 2 つの子があるタスクには、set_ref_count(3) を使用しています。 この追加の参照により、親が暗黙的にプッシュされることを防いでいます。 「継続渡し」にも、暗黙的なプッシュを使用する同様の例があります。 2 つの子があるタスクに set_ref_count(2) を使用しているため、子が完了するとそのタスクが自動的に実行されます。

要約すると、タスク・スケジューラーの基本的な手法は、"幅優先のスチールと深さ優先の作業" です。 幅優先のスチール規則は、スレッドが十分に稼動状態であるような並列処理をもたらします。 幅優先の作業規則では、いったん十分な作業を取得してしまえば、スレッドがそれを効率良く操作できるようにします。

関連情報

[8] 両端キュー。