Task クラスと TaskFactory クラス

Last Updated 2011/09/21



タスクとは何かについては、「用語の解説」で説明したとおり、並列処理の管理を抽象化するために導入された概念です。これによりスレッドを直接操作する必要がなくなりました。.NET Framework の内部でどんな処理をしているかを詮索してもしかたありませんので、これ以上の説明は避けたいと思います。

Note Parallel クラスは一見するとタスクとは関係ないように見えますが、内部的にはタスクを作成しています。

タスクの作成

タスクオブジェクトを作成・実行する手順は以下の 2 つです。

  1. Task クラスのコンストラクタを使って作成し、Start メソッドを呼び出してタスクを実行する
  2. Task クラスの static な Factory プロパティから TaskFactory オブジェクトを取得し、StartNew メソッドの戻り値として取得する

Task オブジェクトを作成する手順は 2. が一般的です。1. と 2. に決定的な違いがあるわけではありませんが、2 のほうがのちのち便利であることが分かります。そこで、このページでは原則として 2. の手順を使うことにします。

Note "Task.Factory.StartNew" vs "new Task(...).Start" というタイトルの WEB ページの中で、タスクを作成するとき、どちらを使うほうがよいかについて述べていますが、ここでも 2. をすすめています。ただし、1. ではダメな理由はそれほどハッキリしているわけではありません。

タスクを作成する基本的な手順は、次の「タスクを実行するデリゲート」をご覧ください。


タスクを実行するデリゲート

タスクを実行するデリゲートは Action<T> タイプか Func<T, TResult> タイプかのどちらかです。後者はタスクの実行結果を返します。

タスクを実行するデリゲートを別の角度からみると、次の 3 種類に分類できます。ただし、以下の用語は権威ある筋のものではなく、私が勝手に付けたものであることをお断りしておきます。

いずれがすぐれているかは一概に言えません。私自身はメソッド構文を好みます。コードの見易さを重視するからです。ダラダラと長いラムダ式を見ることがありますが、ひどく見にくいと感じます。ただし、メソッド構文ではローカル変数の受け渡しができないことがありますからそのような場合はラムダ式構文が便利かもしれません。

メソッド構文

メソッド構文は、メソッドを定義しておいて、それをデリゲートとして呼び出します。

private void buttonl_Click(object sender, RoutedEventArgs e)
{
  var task = Task.Factory.StartNew(this.Method);

  try
  {
    task.Wait();
  }
  catch (AggregateException ae)
  {
    ae.Handle((x) =>
    {
      Debug.WriteLine("AggregateException が発生しました。");
      return true;
    });
  }

  task.Dispose();
}

private void Method()
{
  Debug.WriteLine("Method");
}

匿名メソッド構文

匿名メソッド構文は、実行するデリゲートをインラインで定義します。

private void buttonl_Click(object sender, RoutedEventArgs e)
{
  var task = Task.Factory.StartNew(
    delegate
    {
      Debug.WriteLine("Anonymous Method");
    });

  try
  {
    task.Wait();
  }
  catch (AggregateException ae)
  {
    ae.Handle((x) =>
    {
      Debug.WriteLine("AggregateException が発生しました。");
      return true;
    });
  }

  task.Dispose();
}

ラムダ式構文

ラムダ式構文は、実行するデリゲートをラムダ式で定義します。

private void buttonl_Click(object sender, RoutedEventArgs e)
{
  var task = Task.Factory.StartNew(() => Debug.WriteLine("Lamda Delegate"));

  try
  {
    task.Wait();
  }
  catch (AggregateException ae)
  {
    ae.Handle((x) =>
    {
      Debug.WriteLine("AggregateException が発生しました。");
      return true;
    });
  }

  task.Dispose();
}

TaskFactory クラス

.NET Framework のクラスの中に、"〜 Factory" と名付けられたクラスがありますが、これはクラスの操作をより簡単にするためのクラスです。TaskFactory クラスもその例に漏れず、Task クラスのインスタンスの作成および実行、タスクのキャンセル、継続タスクの設定などを支援するためのプロパティやメソッドが用意されています。

TaskFactory クラスの使用例はアチコチの使用例をもってかえますが、こんな感じという意味で一つの例を以下に示しておきます。

private void buttonl_Click(object sender, RoutedEventArgs e)
{
  var tokenSource = new CancellationTokenSource();
  var token = tokenSource.Token;

  // 子タスク用タスクファクトリ
  var taskFactory = new TaskFactory(token, TaskCreationOptions.PreferFairness,
      TaskContinuationOptions.AttachedToParent, TaskScheduler.Default);

  var task = Task.Factory.StartNew(() =>
  {
    var childTask1 = taskFactory.StartNew(() =>
    {
      Debug.WriteLine("childTask1 を実行するデリゲート");
    });
  });

  try
  {
    task.Wait();
  }
  catch (AggregateException ae)
  {
    ae.Handle((x) =>
    {
      Debug.WriteLine("AggregateException が発生しました。");
      return true;
    });
  }

  task.Dispose();
}

戻り値を返すタスク

タスクを実行するデリゲートで説明したとおり、戻り値を返すタスクのデリゲートは Func<T, TResult> タイプでなければなりません。戻り値は、Task<TResult> オブジェクトです。

private void buttonl_Click(object sender, RoutedEventArgs e)
{
  Task<int> task = Task<int>.Factory.StartNew(() => 123);

  try
  {
    task.Wait();
  }
  catch (AggregateException ae)
  {
    ae.Handle((x) =>
    {
      Debug.WriteLine("AggregateException が発生しました。");
      return true;
    });
  }

  textBox.Text = String.Format("task の戻り値:{0}\r\n", task.Result);

  task.Dispose();
}

継続タスク

並列コンピューティング用語としての継続に対する原語は "continuation" です。継続タスクとは継続元 "antecedent" のタスクから呼び出されるタスクをさします。

以下のコードは、特定のタスクから継続する例です。コード中に書きましたが、継続タスクは継続元のタスクと関連付けられましたから Start メソッドを呼び出す必要はありません。継続元の実行結果(antecendent)を継続タスクに受け渡しています。

private void button1_Click(object sender, RoutedEventArgs e)
{
  Task<DayOfWeek> task = Task<DayOfWeek>.Factory.StartNew(() => DateTime.Today.DayOfWeek);

  Task<string> continuationTask = task.ContinueWith((antecedent) =>
  {
    return String.Format("今日は {0} です。", antecedent.Result);
  });

  // continuatioTask は task の継続タスクなので、Start メソッドの呼び出しは不要

  // Result プロパティを参照しているので、ここで自動的に待機する(Wait メソッドの呼び出しは不要)
  Debug.WriteLine(String.Format("continuationTask の実行結果: {0}", continuationTask.Result));

  task.Dispose();
  continuationTask.Dispose();
}

次のコードは、3 つのタスクを同時に実行し、すべてのタスクの終了を待って、継続タスクを実行します。継続タスクでは 3 つのタスクの実行結果を合計します。

private void button1_Click(object sender, RoutedEventArgs e)
{
  Task<int> task1 = Task<int>.Factory.StartNew(() => { return 1; });
  Task<int> task2 = Task<int>.Factory.StartNew(() => { return 2; });
  Task<int> task3 = Task<int>.Factory.StartNew(() => { return 3; });

  Task<int>[] tasks = new Task<int>[] { task1, task2, task3 };

  // すべてのタスクが終了したあと、継続するタスクを作成する
  Task<int> continuationTask = Task<int>.Factory.ContinueWhenAll(
      tasks,
      (antecedents) => { return antecedents.Sum(n => n.Result); } // 3 つのタスクの実行結果を合計する
  );

  // この出力は 6 である
  Debug.WriteLine(String.Format("{0}", continuationTask.Result));

  task1.Dispose();
  task2.Dispose();
  task3.Dispose();
  continuationTask.Dispose();
}

子タスク

継続タスクの最初のコード例の continuationTask は子タスクの一種で、継続元タスクがすでに起動していますから Start メソッドを呼び出す必要はありませんでした。このようにタスクのレベルが同じでも親子関係があります。

一方、タスクは入れ子にすることも可能です。このとき、親タスクと親子関係を持つかどうかを指定することができます。以下は、入れ子にはしますが、親子関係を確立しない例です。

private void button1_Click(object sender, RoutedEventArgs e)
{
  var task= Task.Factory.StartNew(() =>
  {
    Debug.WriteLine("親タスクの実行を開始");

    var childTask = Task.Factory.StartNew(() =>
    {
      Debug.WriteLine("入れ子のタスクの実行を開始");
      Thread.SpinWait(10000000);
      Debug.WriteLine("入れ子のタスクの実行を終了");
    });

    Thread.SpinWait(10000000);
  });

  task.Wait();
  Debug.WriteLine("親タスクの実行を終了");

  task.Dispose();
}

実行結果:

親タスクの実行を開始
入れ子のタスクの実行を開始
親タスクの実行を終了
入れ子のタスクの実行を終了

実行結果を見ると、親タスクと子タスクとは関係なく実行を開始し、終了しています。

次は、子タスクと親子関係を確立する例です。

private void button1_Click(object sender, RoutedEventArgs e)
{
  var parentTask = Task.Factory.StartNew(() =>
  {
    Debug.WriteLine("親タスクの実行を開始");

    var childTask = Task.Factory.StartNew((t) => // 親タスクにアタッチするので、親タスクを受け取らなければならない
    {
      Debug.WriteLine("入れ子のタスクの実行を開始");
      Thread.SpinWait(1000000);
      Debug.WriteLine("入れ子のタスクの実行を終了");
    }, TaskContinuationOptions.AttachedToParent); // 親タスクにアタッチする

    Thread.SpinWait(10000000);
  });

  parentTask.Wait();
  Debug.WriteLine("親タスクの実行を終了");

  parentTask.Dispose();
}

実行結果:

親タスクの実行を開始
入れ子のタスクの実行を開始
入れ子のタスクの実行を終了
親タスクの実行を終了

親タスクは子タスクの実行終了を待って終了しています。


TaskCompletionSource<TResult> クラス

このクラスは、特定のデリゲートと関連付けない Task<TResult> オブジェクトを作成する一般的な手順を提供します。

このクラスは戻り値を返すタスクを作成するのですが、チョット変わっていて使いやすいのかどうかがよく分かりません。タスクの戻り値を設定するメソッドやキャンセルするメソッドを提供しますが、詳しくは、NETClass を参照してください。

private void buttonl_Click(object sender, RoutedEventArgs e)
{
  TaskCompletionSource<int> completionSource = new TaskCompletionSource<int>();
  Task<int> task1 = completionSource.Task;

  // タスクを特定せず、並列処理を実行する
  Task.Factory.StartNew(() =>
  {
    Thread.Sleep(1000);
    completionSource.SetResult(15); // task1 の結果を設定する
  });

  Debug.WriteLine(String.Format("task1.Result= {0}", task1.Result));

  // ------------------------------------------------------------------

  // ワザと例外を発生する
  TaskCompletionSource<int> completionSource2 = new TaskCompletionSource<int>();
  Task<int> task2 = completionSource2.Task;

  Task.Factory.StartNew(() =>
  {
    Thread.Sleep(1000);
    completionSource2.SetException(new InvalidOperationException("SIMULATED EXCEPTION"));
  });

  try
  {
    int result = task2.Result;
    Debug.WriteLine("task2.Result の取得に成功(これは期待する動作ではない)");
  }
  catch (AggregateException ex)
  {
    Debug.WriteLine("次の例外が発生した。");

    for (int i = 0; i < ex.InnerExceptions.Count; ++i)
    {
      Debug.WriteLine(String.Format("-------------------------------\n{0}", ex.InnerExceptions[i].ToString()));
    }
  }

  task1.Dispose();
  task2.Dispose();
}

実行結果:

task1.Result= 15
次の例外が発生した。
-------------------------------
System.InvalidOperationException: SIMULATED EXCEPTION

TaskCreationOptions enum 型

TaskCreationOptions enum 型はタスクを作成するとき、どのように作成するかを指定するオプションです。

定数意味
None0デフォルトの動作
PreferFairness1先にスケジューリングしたタスクから順番に実行
LongRunning2実行時間のかかる処理
AttachedToParent4タスク階層の親タスクにアタッチ

None はデフォルトの動作をするものらしいのですが、デフォルトの動作が何なのかは不明です。特に何も設定しない状態と理解するほかありません。

PreferFairness は、「先にスケジューリング」の意味がハッキリしません。コードの上から順番でいいのでしょうか。あるいは、コンパイラの都合なのでしょうか。普通に考えればタスクを開始した順番なのでしょうが、タスクを開始する順番をアプリケーションから制御できるのでしょうか。ここで思い出しましたが、ワークスティーリング(用語の解説を参照)をするとスケージューリングの順番が狂ってきますが、これを抑制するという意味なのかもしれません。これが正しいとすれば、数は多いが、一つ一つのタスクが小さい場合に指定するといいのかもしれません。

LongRunning は、どれぐらいの時間を「時間のかかる処理」というのでしょうか。.NET Framework SDK の解説では、「タスクスケジューラに対してオーバーサブスクリプションを許可してもよいことを示します」とありますが、「オーバーサブスクリプション」って何でしょうね。

Note サブスクリプション "subscription" は、「予約」でいいと思います。CPU に対してコアを使いたいとの要求ですね。オーバーサブスクリプションは、「現在の CPU のコア数を上回るコアを予約する」でどうでしょうか。

ところで、Microsoft のサイトで "TPLOptionsTour.pdf" というドキュメントを見つけました。その中に LongRunning を指定したほうがよい基準が書いてあります。原文は英語ですが、日本語にしておきます。ただし、このオプションを設定すると必ず高速になるとは限らないので、動作を確認することとあります。

これは想像ですが、CPU のコア数が多い場合に、積極的に処理を分散するようにするというコンパイラへの指示ではないかと思います。いずれにしろ、このオプションは時間のかかる処理だからといって必ず設定しなければならないようなものではないと思います。

Note 時間がかかる処理をする場合は、このオプションを指定する場合と指定しない場合とを比較してみてください。それ以外にこのオプションのメリットを説明しようがありません。

AttachedToParent はタスクを入れ子にした子タスクにアタッチ、つまり、親子関係を持つかどうかを指定するものです。つまり、これを指定すると、親タスクはすべての子タスクが終了するまで終了しません。また、子タスクで発生した例外は親タスクに伝達されます。以下は、このオプションの設定例です。

private void button1_Click(object sender, RoutedEventArgs e)
{
  var outer = Task.Factory.StartNew(() =>
  {
    Debug.WriteLine("Outer task beginning.");

    var child = Task.Factory.StartNew(() =>
    {
      Thread.SpinWait(5000000);
      Debug.WriteLine("Detached task completed.");
    });

  });

  outer.Wait();
  Debug.WriteLine("Outer task completed.");
}

実行結果:
Outer task beginning.
Outer task completed.   ← 子タスクの実行を待たない
Detached task completed.

//----------------------------------------------------------------
private void button2_Click(object sender, RoutedEventArgs e)
{
  var outer = Task.Factory.StartNew(() =>
  {
    Debug.WriteLine("Outer task beginning.");

    var child = Task.Factory.StartNew(() =>
    {
      Thread.SpinWait(5000000);
      Debug.WriteLine("Detached task completed.");
    }, TaskCreationOptions.AttachedToParent); // ← タスク作成時のオプションを追加

  });

  outer.Wait();
  Debug.WriteLine("Outer task completed.");
}

実行結果:
Outer task beginning.
Detached task completed.
Outer task completed.   ← 子タスクの実行を待ってから終了する

なお、enum 型は FlagsAttribute 属性を持ちますので、Or 演算子で組み合わせることができます。


TaskContinuationOptions enum 型

定数意味
None0x00デフォルトの動作(直前のタスクのステータスに関係なく、前のタスクの終了時にスケジュールされる)
PreferFairness0x01スケジュールの順番をできるだけ維持する
LongRunning0x02実行時間のかかる処理
AttachedToParent0x04タスク階層の親タスクにアタッチ
NotOnRanToCompletion0x10000前のタスクが終了するまで継続タスクをスケジュールしない
NotOnFaulted0x20000前のタスクでハンドルされない例外がスローされた場合は継続タスクをスケジュールしない
NotOnCanceled0x40000前のタスクが取り消された場合は継続タスクをスケジュールしない
OnlyOnRanToCompletion0x60000(= NotOnCanceled | NotOnFaulted)
前のタスクが終了したときのみ継続タスクをスケジュールする
OnlyOnFaulted0x50000(= NotOnRanToCompletion | NotOnCanceld)
前のタスクで処理しなかった例外がスローされた場合にのみ継続タスクをスケジュールする
OnlyOnCanceled0x30000(= NotOnRanToCompletion | NotOnFaulted)
前のタスクを取り消した場合にのみ継続タスクをスケジュールする
ExecuteSynchronously0x80000継続タスクを同期的に実行する

PreferFairness、LongRunning、AttachedToParent については、TaskCreationOptions enum 型の項を参照してください。

NotOnCanceled はたとえば、継続元タスクの実行結果を受けてそれを出力する継続タスクに対して指定します。つまり、継続元のタスクがキャンセルされているのに出力する必要がないからです。

Note キャンセルといっても意図的な場合とそうでない場合とがあります。OperationCanceledException 例外が発生する場合はユーザーが意図的にキャンセルするものですが、継続元タスクが継続タスクを実行する必要がないと判断する場合はプログラミング上の都合によるキャンセルです。

OnlyOnFaulted は、継続元のタスクで発生した例外を捕捉するときに指定するといいでしょう。ここでどんな例外が発生したかを出力したり、例外を処理済みにしてしまえば例外の発生による異常終了を避けることができます。

ExecuteSynchronously は、継続元のタスクと同じスレッド上で実行することです。つまり、通常は継続元タスクと継続タスクとは異なるスレッド上で実行されますが、継続タスクの処理時間が短い場合はこのオプションを指定することで別スレッドを作成する手間を省略することができます。


タスクの待機メソッド

待機メソッドとは、Task クラスの Wait、WaitAll、WaitAny メソッドをさします。待機メソッドの使い方についてはここまでのコード例の中にも出てきましたから分かると思いますが、重要な点を指摘しておかなければなりません。タスクの実行中に例外が発生した場合に例外を捕捉するには待機メソッドを try ... catch 文内で使わなければならないということです。以下は、これまでのコード例にもありましたが、待機メソッド前後の典型的なコードと理解してください。

  ....
  ....

  try
  {
    task.Wait();
  }
  catch (AggregateException ae)
  {
    ae.Handle((x) =>
    {
      Debug.WriteLine("AggregateException が発生しました。");
      return true;
    });
  }

並列処理における例外の処理は、「並列処理の例外処理」のページで詳しく説明します。


−以上−