xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

TPL入門 (10) - タスクの継続

あるタスクが完了したあと、連続して別のタスクを実行したい場合があります。このとき、まず思い付くのは「最初に実行したタスクAを待機して、それから別のタスクBを開始する」でしょう。しかし、これでは結局呼び出し元スレッドをブロックすることになり、最初のタスクAが呼び出し元スレッドと並列に実行されている意味がなくなります。長い処理が行われる場合でもUIスレッドをブロックすることなく処理を進めることは、ユーザー体験の重要な項目のひとつです。そこで今回は、呼び出し元スレッドをブロックすることなく、タスクの完了とともに別のタスクを連続して実行する方法を紹介します。

継続実行

タスクを継続して実行するには、TaskクラスのContinueWithメソッドを利用します。次のサンプルを見てください。

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
 
namespace Sample17_TaskContinueWith
{
    class Program
    {
        static void Main()
        {
            Console.WriteLine("Main : Begin");
            var task = new Task<int>(() =>
            {
                Console.WriteLine("Task1 : Begin");
                var sum = Enumerable.Range(1, 10000).Sum();
                Console.WriteLine("Task1 : End");
                return sum;
            });
            task.Start();
            task.ContinueWith(task1 =>
            {
                Console.WriteLine("Task2 : Begin");
                Console.WriteLine("Sum = {0}", task1.Result);
                Console.WriteLine("Task2 : End");
            });
            Console.WriteLine("Main : End");
            Thread.Sleep(1000);
        }
    }
}
 
//----- 結果
/*
Main : Begin
Main : End
Task1 : Begin
Task1 : End
Task2 : Begin
Sum = 50005000
Task2 : End
*/

上記のサンプルでは、1~10000までの合計を計算するタスク(Task1)と、結果を表示するタスク(Task2)を分けています。しかし、これらふたつのタスクの完了を待機するために呼び出し元スレッドがブロックされることはありません。結果を見るために1秒間スレッドを意図的に停止していますが、それはWaitメソッドなどとは意味が違うので注意してください。また、Task1がContinueWithメソッドを呼び出すよりも先に完了してしまう可能性がありますが、ContinueWithメソッドはTask1が完了していることを検知した上でTask2を開始するので問題ありません。

また、次のようにひとつのTaskインスタンスに対して複数回ContinueWithメソッドを呼び出すことができます。Taskインスタンスは内部的に継続実行タスクのコレクションを保持できるようになっており、タスクが完了するとそれらすべてを実行しにかかります。

using System;
using System.Threading;
using System.Threading.Tasks;
 
namespace Sample18_TaskContinueWithMultiple
{
    class Program
    {
        static void Main()
        {
            Console.WriteLine("Begin");
            var task = new Task(() => Console.WriteLine("Task1"));
            task.Start();
            task.ContinueWith(task1 => Console.WriteLine("Task2"));
            task.ContinueWith(task1 => Console.WriteLine("Task3"));
            Console.WriteLine("End");
            Thread.Sleep(1000);
        }
    }
}
 
//----- 結果 (例)
/*
Begin
End
Task1
Task3
Task2
*/

継続オプションの指定

ContinueWithメソッドには、TaskContinuationOptions列挙体を引数に指定できるオーバーロードがあります。TaskContinuationOptions列挙体を利用することで、継続タスクの動作を指定する変更することができます。特に便利なのが次のオプションです。

オプション 説明
OnlyOnRanToCompletion 前のタスクが完了まで実行された場合にのみ、継続タスクをスケジュールするように指定
OnlyOnFaulted 前のタスクでハンドルされない例外がスローされた場合にのみ、継続タスクをスケジュールするように指定
OnlyOnCanceled 前のタスクが取り消された場合にのみ、継続タスクをスケジュールするように指定

これらを利用することで、成功時、エラー時、キャンセル時の処理を条件分岐なしにスマートに記述することができます。また言うまでもありませんが、タスク完了時、継続実行タスクのうち条件に合わないものは自動的にキャンセルされます

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
 
namespace Sample19_TaskContinuationOptions
{
    class Program
    {
        static void Main()
        {
            Console.WriteLine("Begin");
            var task = Task<int>.Factory.StartNew(() => Enumerable.Range(1, 10000).Sum());
            task.ContinueWith
            (
                task1 => Console.WriteLine("Success : {0}", task1.Result),
                TaskContinuationOptions.OnlyOnRanToCompletion
            );
            task.ContinueWith
            (
                task1 => Console.WriteLine("Error : {0}", task1.Exception),
                TaskContinuationOptions.OnlyOnFaulted
            );
            task.ContinueWith
            (
                task1 => Console.WriteLine("Task was canceled."),
                TaskContinuationOptions.OnlyOnCanceled
            );
            Console.WriteLine("End");
            Thread.Sleep(1000);
        }
    }
}
 
//----- 結果
/*
Begin
End
Success : 50005000
*/

次回予告

今回は呼び出し元スレッドをブロックすることなくタスクを連続して実行する方法を紹介しました。次回はタスクを入れ子にしたり、親子関係を持つタスクについて触れてみようと思います。