Ver. 4.0
C# 3.0 で導入されたラムダ式と、 .NET 4 で導入された Task、Parallel、ParallelEnumerable などのクラスを使うことで、 非同期処理や並列処理が簡潔に記述できるようになりました。
また、C# 5.0 では非同期処理用の新構文が追加される予定です。 参考: 「非同期処理」
このページでは、これら非同期処理の基礎となる Task クラスや、その背後にあるスレッド プールというものについて説明していきます。
スレッドを使った非同期処理を行いたい動機としては、以下の2つが挙げられます。
どちらの場合でも、非同期で行いたい処理のほとんどは小さい処理になります。 (1つの処理を長時間し続けるよりは、細々とした処理を大量に扱う場合が多い。)
この細々とした処理の1つ1つのことをタスク(task)と呼びます。 このページで説明する、.NET 4 の Task クラスもこの「タスク」を表すクラスです。
まず、非同期処理の基本となるスレッドについて説明しておきましょう。
スレッド(thread)は、CPU の数を超えて複数の処理を同時に実行するための仕組みです。 例えば、単一 CPU で4つの処理を実行する場合の例を図3に示します。
同時に実行するといっても、本当に並列に動いているわけではありません。 一定間隔で OS が処理を奪い、別のスレッドに切り替えることで、見かけ上の同時実行を実現しています。 (ハードウェア割り込みというものを使っていて、例えあるスレッドがフリーズしていても、強制的に OS に処理が移るようになっています。) (ちなみに、このようなスレッドの切り替えをコンテキスト スイッチ(context switch: 実行文脈の切り替え)と呼びます。)
このような挙動を実現するためには、以下のように、それなりのコストがかかります。
タスクのスケジューリング(どうやってタスクの同時実行を行うか、タスク切り替え方の管理方法)には大きく分けて2種類あります。
本節で説明したスレッドというものは、前者のプリエンプティブ マルチタスクになります。
また、次節で説明するスレッド プールは、両者の併用になります。 プリエンプティブなスレッドの上に、協調的にスレッドを使いまわすような仕組みを作ることで、 公平性を残しつつ、コンテキスト スイッチの負荷を最小限に抑えます。
前述の通り、スレッドは、生成も切り替えも、それなりに(そして、多くの人が思っている以上に)コストがかかります。 スレッドの増加はパフォーマンスへの影響が非常に大きく、スレッドの数は最小限に抑えたいです。
そこで、実際には、スレッドを直接使うのではなく、 1度作ったスレッドを可能な限り使いまわすような仕組みを使います。 このようなスレッドの使い回しの仕組みをスレッド プール(thread pool)と呼びます。
スレッド プールとは、以下のような仕組みです。
ちなみに、BeginInvoke によるデリゲートの非同期呼び出し( 「非同期呼び出し」 参照)や、 後述する Task クラスは内部的にこのスレッド プールを使っています。 その他、Timer クラスによるタイマー処理や、 WebClient クラスなどによる I/O 待ちの非同期処理でもスレッド プールが利用されます。
スレッド プールの仕組みは昔からありましたが、 .NET Framework 4 では性能改善のためにスレッド プールの再設計・実装が行われました。 (具体的には、後述するワーク スティーリングという仕組みで性能改善を図ります。)
.NET Framework 4 のスレッド プールでは、図4に示すように、スレッドごとにローカルなキューを持っています。
タスク実行用のスレッド(ワーカー スレッド(worker thread)と呼びます)から新しいタスクを追加する場合、 タスクはローカル キューに投入されます。 一方、ワーカー スレッド以外からのタスクの追加はグローバル キューに投入されます。
各スレッドは、現在のタスクが完了すると、まず、ローカル キューを見に行きます。 ローカル キューにタスクがなければ、次に、他のスレッドのキューを見に行き、 もしそちらにタスクがあれば、タスクを奪い取ります。 このような挙動をワーク スティーリング(work stealing: 仕事を奪い取る)と呼びます。
また、全てのスレッドのローカル キューが空ならば、グローバル キューからタスクを取り出して実行します。
ここで、ローカル キューからの取り出しと、他スレッドからのスティーリングとでは、 タスクの取り出しの向きを変えます。 ローカルの場合には FIFO(first in first out: 先入れ先出し)、 スティーリングの場合には FILO(first in last out: 先入れ後出し)になっています。 このことで、以下のような効果が得られます。
.NET Framework 4 では、スレッド プールをより使いやすくするために、Task(System.Threading.Tasks 名前空間)というクラスが導入されました。 Task クラスは以下のような機能を持っています。
非同期処理の結果を使いたい場合があります。 Task クラスからの結果の受け取り方には2通りの方法があります。
1つは、ContinueWith メソッドを使って、タスク完了時にその先続けて行いたい処理を渡します。
var t = Task.Factory.StartNew(() => { // 何か重たい計算をして、その計算結果を返す。 return HeavyWork(); }); // 計算が完了したら、そのあと続けたい処理を呼び出してもらう。 t.ContinueWith(x => Console.WriteLine(x.Result));
もう1つは、タスクの完了を同期的に(完了するまで処理を止めて)待ちます。 Result プロパティを読もうとしたとき、タスクがまだ完了していない場合、 完了するまで待つことになります。
var t = Task.Factory.StartNew(() => { // 何か重たい計算をして、その計算結果を返す。 return HeavyWork(); }); // 同期的に完了を待つ。 Console.WriteLine(t.Result);
非同期実行中のタスクを途中でキャンセルするための仕組みとして、 CancellationToken 構造体というものが標準で用意されています。
var cts = new CancellationTokenSource(); var t = Task.Factory.StartNew(() => { Thread.Sleep(500); Console.WriteLine("done"); }, cts.Token); // t をキャンセル cts.Cancel();
タスクの中で別の新しいタスクを作りたい場合があります。 オプションなしの場合、それぞれのタスクは無関係に動くことになります。
var t = Task.Factory.StartNew(() => { Console.WriteLine("タスク1開始"); Task.Factory.StartNew(() => { Console.WriteLine("タスク2開始"); }); }); t.Wait(); // 今のままだと、タスク2の完了は待たない Console.WriteLine("完了");
タスク1開始 完了
これに対して、オプションを指定することで、タスクに親子関係を作ることができます。 Task.Wait による完了待ちは、子タスクの完了まで含めて待ちます。
var t = Task.Factory.StartNew(() => { Console.WriteLine("タスク1開始"); Task.Factory.StartNew(() => { Console.WriteLine("タスク2開始"); }, TaskCreationOptions.AttachedToParent); // 子タスク化 }); t.Wait(); // 子タスクの完了まで待つ Console.WriteLine("完了");
タスク1開始 タスク2開始 完了
Task クラスでは、タスク開始時に TaskScheduler を渡すことで、 タスクの実行方法をある程度柔軟に制御できます。
特に指定しない場合、タスクはスレッド プール上で実行されます。 一方、 「[雑記] GUI と非同期処理」 で説明するように、 ある特定のスレッド上で実行する必要がある処理もあり、 その特定スレッドにタスクを投かんするような仕組みが必要です。 そういう場合に、TaskScheduler を利用します。
var t = new Task(() => { /* 中略 */ }); // タスクの実行場所を制御するために明示的に TaskScheduler を指定 t.Start(TaskScheduler.FromCurrentSynchronizationContext());
Task クラスは以下のような場面で使われます。
.NET Framework 4で導入された、Parallel クラスや ParallelEnumerable クラス(通称 Parallel LINQ)などの並列処理を用ライブラリは、 Task クラスの上に実装されています。 並列処理の詳細は 「並列処理ライブラリ」 にて説明します。
「[雑記] 継続と先物」 参照。
(書きかけ)
並列、非ブロッキング処理と、あともう1つ、データ フロー http://blogs.msdn.com/b/pfxteam/archive/2010/10/28/10081950.aspx Actor とか Agent って言われるもの 非同期に動き続けてる Agent が何個かいて、データをやり取りしながら各々が自律的にデータ処理 producer/consumer パターン