前回まではデータの並列化 (= コレクションの各要素の並列処理) について見てきましたが、今回からはタスクの並列化 (= 他の処理とは独立に実行可能な作業の並列処理) について見ていきます。
タスクの概要
これまで、非同期処理/並列処理を行う際にはThreadやThreadPoolが利用されてきました。しかし、これらには処理の完了を知るための標準的な方法がない、処理の戻り値を取得する方法がないなどの問題点や制限事項がありました。TPLでは、これらの弱点を克服し、さらに拡張性やパフォーマンスを向上させるために「タスク」という概念が導入されています。タスクを利用することで次のような恩恵を受けることができます。
- システムリソースをより効率的かつスケーラブルに利用可能
- 待機、戻り値の取得、キャンセル、継続、例外処理、詳細な実行状態の把握、スケジューリングが可能
前者は開発者が特に何も意識しなくても最大の処理能力を発揮できるように、TPLが自動的に最適化してくれます。後者はこれから数回にわたって少しずつ見ていく予定です。
タスクの生成と実行
「タスク」はSystem.Threading.Tasks名前空間にあるTaskクラスで定義されています。タスクの生成は非常に簡単です。まずはサンプルを見てみてください。
using System; using System.Threading; using System.Threading.Tasks; namespace Sample10_TaskStart { class Program { static void Main() { var task1 = new Task(() => Console.WriteLine("Task1")); var task2 = new Task(() => Console.WriteLine("Task2")); var task3 = new Task(() => Console.WriteLine("Task3")); task1.Start(); task2.Start(); task3.Start(); Thread.Sleep(1000); } } } //----- 結果 (例) /* Task2 Task1 Task3 */
上記のように、Taskインスタンスの生成時、コンストラクタ引数として非同期に行いたい処理をデリゲートで記述し、生成されたインスタンスのStartメソッドを呼び出して処理を開始するだけです。Startメソッドは呼び出し元スレッドをブロックしないので、実行結果を見るために最後に1秒間スレッドを停止しています。
Startメソッドは処理をスケジューラー (=どういう順序で処理するかを最適化し割り振る人) に登録します。実行順序は登録された処理がどのようにスケジューリングされるかに依存します。今回の結果例からも分かるように、必ずしもStartメソッドを呼び出した順序通りにはならないことに注意してください。 (Startメソッドのオーバーロードには、スケジューラーを設定できるものもあります)
タスクの生成と実行を同時に
上記のサンプルではタスクの生成と実行を別々に行いましたが、次のように生成と実行を同時に行うことも可能です。タスクの生成後、開始を特に遅延させる理由がない場合はこちらを使うと良いと思います。
using System; using System.Threading; using System.Threading.Tasks; namespace Sample11_TaskFactoryStartNew { class Program { static void Main() { var task1 = Task.Factory.StartNew(() => Console.WriteLine("Task1")); var task2 = Task.Factory.StartNew(() => Console.WriteLine("Task2")); var task3 = Task.Factory.StartNew(() => Console.WriteLine("Task3")); Thread.Sleep(1000); } } } //----- 結果 (例) /* Task2 Task1 Task3 */
暗黙的なタスク
ここまでTaskを明示的に生成する方法について見てきましたが、もっと簡単に並列処理を行うやり方があります。それがParallel.Invokeメソッドを利用する方法です。次の例を見てください。
using System; using System.Threading; using System.Threading.Tasks; namespace Sample12_ParallelInvoke { class Program { static void Main() { Console.WriteLine(DebugMessage("Begin")); Parallel.Invoke ( () => { Thread.Sleep(1000); Console.WriteLine(DebugMessage("Task1")); }, () => { Thread.Sleep(3000); Console.WriteLine(DebugMessage("Task2")); }, () => { Thread.Sleep(5000); Console.WriteLine(DebugMessage("Task3")); } ); Console.WriteLine(DebugMessage("End")); } static string DebugMessage(string keyword) { string format = "{0}\\n\\tTime = {1}\\n\\tThread ID = {2}\\n"; return string.Format(format, keyword, DateTime.Now.ToLongTimeString(), Thread.CurrentThread.ManagedThreadId); } } } //----- 結果 (例) /* Begin Time = 22:26:48 Thread ID = 1 Task1 Time = 22:26:49 Thread ID = 3 Task2 Time = 22:26:51 Thread ID = 4 Task3 Time = 22:26:53 Thread ID = 5 End Time = 22:26:53 Thread ID = 1 */
Invokeメソッドの引数に並列処理したい作業をデリゲートで並べて記述するだけです。また、Invokeメソッドは呼び出し元のスレッドをブロックします。そのため、上記のサンプルでもEndが最後に処理されています。「タスクを生成して、実行して、全部終わるまで待機して...」といった一連の煩わしさがすべて排除され、「並列に〇〇と××と△△を実行する」を意図通りに記述できるのが特徴です。
次回予告
今回はタスクの生成と実行について見てきました。次回は明示的に作成したタスクの完了を待機する方法と、タスクから結果を取得する方法について見ていきたいと思います。