継続タスク
更新 : 2010 年 6 月
非同期プログラミングでは、非同期操作で完了時に 2 番目の操作を呼び出してデータを渡すのが一般的です。 これまで、この処理はコールバック メソッドを使用して行っていました。 タスク並列ライブラリでは、継続タスクに同じ機能が用意されています。 継続タスク (単に "継続" とも呼ばれます) とは、別のタスク ("継続元" と呼ばれます) が完了したときにそのタスクによって呼び出される非同期タスクのことです。
継続は比較的簡単に使用できますが、強力な機能と柔軟性を備えています。 たとえば、次のように操作できます。
継続元のデータを継続に渡す
継続を呼び出す場合、呼び出さない場合についての正確な条件を指定する
継続が開始される前、または継続の実行中に継続を取り消す
継続をスケジュールする方法についてのヒントを提供する
同じ継続元から複数の継続を呼び出す
複数の継続元のすべてまたはいずれかが完了したときに 1 つの継続を呼び出す
任意の長さで連続して継続を実行する
継続元によってスローされた例外を処理するために継続を使用する
Task.ContinueWith メソッドを使用して継続を作成します。 次の例は、基本的なパターンを示しています (わかりやすくするために、例外処理は省略されています)。
' The antecedent task. Can also be created with Task.Factory.StartNew.
Dim taskA As Task(Of DayOfWeek) = New Task(Of DayOfWeek)(Function()
Return DateTime.Today.DayOfWeek
End Function)
' The continuation. Its delegate takes the antecedent task
' as an argument and can return a different type.
Dim continuation As Task(Of String) = taskA.ContinueWith(Function(antecedent)
Return String.Format("Today is {0}", antecedent.Result)
End Function)
' Start the antecedent.
taskA.Start()
' Use the contuation's result.
Console.WriteLine(continuation.Result)
// The antecedent task. Can also be created with Task.Factory.StartNew.
Task<DayOfWeek> taskA = new Task<DayOfWeek>(() => DateTime.Today.DayOfWeek);
// The continuation. Its delegate takes the antecedent task
// as an argument and can return a different type.
Task<string> continuation = taskA.ContinueWith((antecedent) =>
{
return String.Format("Today is {0}.",
antecedent.Result);
});
// Start the antecedent.
taskA.Start();
// Use the contuation's result.
Console.WriteLine(continuation.Result);
また、次の例に示すように、タスクの配列のいずれかまたはすべてが完了したときに実行する複数のタスクの継続を作成することもできます。
Dim task1 As Task(Of Integer) = New Task(Of Integer)(Function()
' Do some work...
Return 34
End Function)
Dim task2 As Task(Of Integer) = New Task(Of Integer)(Function()
' Do some work...
Return 8
End Function)
Dim tasks() As Task(Of Integer) = {task1, task2}
Dim continuation = Task.Factory.ContinueWhenAll(tasks, Sub(antecedents)
Dim answer As Integer = tasks(0).Result + tasks(1).Result
Console.WriteLine("The answer is {0}", answer)
End Sub)
task1.Start()
task2.Start()
continuation.Wait()
Task<int>[] tasks = new Task<int>[2];
tasks[0] = new Task<int>(() =>
{
// Do some work...
return 34;
});
tasks[1] = new Task<int>(() =>
{
// Do some work...
return 8;
});
var continuation = Task.Factory.ContinueWhenAll(
tasks,
(antecedents) =>
{
int answer = tasks[0].Result + tasks[1].Result;
Console.WriteLine("The answer is {0}", answer);
});
tasks[0].Start();
tasks[1].Start();
continuation.Wait();
継続は WaitingForActivation の状態で作成されるため、継続元タスクによってのみ開始されます。 ユーザー コードの継続で Task.Start を呼び出すと、System.InvalidOperationException が発生します。
継続はそれ自体が Task であり、継続が開始されたスレッドをブロックするわけではありません。 継続タスクが完了するまでブロックするには、Wait メソッドを使用します。
継続のオプション
単一タスクの継続を作成する場合、System.Threading.Tasks.TaskContinuationOptions 列挙型を取得する ContinueWith オーバーロードを使用して、継続元タスクで継続を開始するための条件を指定できます。 たとえば、継続元が完了まで実行された場合や、違反状態で完了した場合にのみ継続を実行するように指定できます。 継続元で継続を呼び出す準備ができているときに条件が true でない場合、継続は直接 Canceled 状態に遷移し、継続タスクを開始できません。 複数のタスクの継続で NotOn オプションまたは OnlyOn オプションを指定した場合は、実行時に例外がスローされます。
System.Threading.Tasks.TaskContinuationOptions 列挙型には、System.Threading.Tasks.TaskCreationOptions 列挙型と同じオプションが含まれます。 AttachedToParent、LongRunning、および PreferFairness には、両方の列挙型と同じ意味と値が含まれます。 これらのオプションは、複数のタスクの継続で使用できます。
次の表に TaskContinuationOptions のすべての値を示します。
要素 |
説明 |
---|---|
TaskContinuationOptions が指定されていない場合に、既定の動作を指定します。 継続は、継続元の最終的な状態に関係なく、継続元の完了時にスケジュールされます。 タスクに子タスクがある場合は、デタッチされた入れ子のタスクとして作成されます。 |
|
継続のスケジュールを、先にスケジュールされたタスクが先に実行され、後からスケジュールされたタスクは後から実行されるように指定します。 |
|
継続が、実行に時間のかかる、細分化されていない操作であることを示します。 これは、System.Threading.Tasks.TaskScheduler に対し、オーバーサブスクリプションを許可してもよいことを示します。 |
|
継続が子タスクの場合は、タスク階層内の親にアタッチするように指定します。 継続元も子タスクである場合にのみ、継続が子タスクになります。 |
|
継続元が完了まで実行された場合は、継続をスケジュールしないように指定します。 |
|
継続元でハンドルされない例外がスローされた場合は継続をスケジュールしないように指定します。 |
|
継続元が取り消された場合は、継続をスケジュールしないように指定します。 |
|
継続元が完了まで実行された場合にのみ継続をスケジュールするように指定します。 |
|
継続元でハンドルされない例外がスローされた場合にのみ継続をスケジュールするように指定します。 OnlyOnFaulted オプションを使用すると、継続元の Exception プロパティが null でないことが保証されます。 このプロパティを使用すると、例外をキャッチして、タスクの違反の原因となった例外を確認できます。 Exception プロパティにアクセスしない場合、例外は処理されなくなります。 また、キャンセルまたは違反になったタスクの Result プロパティにアクセスしようとすると、新しい例外が発生します。 |
|
継続元が Canceled 状態で完了した場合にのみ継続がスケジュールされるように指定します。 |
|
非常に短時間で完了する継続に使用します。 前のタスクが終了状態に遷移するように、できる限り継続が同じスレッドで実行されるように指定します。 継続の作成時に継続元が既に完了している場合、継続はその継続を作成したスレッド上で実行が試みられます。 継続元の CancellationTokenSource が finally ブロック (Visual Basic では Finally) で破棄された場合、このオプションを指定した継続がその finally ブロックで実行されます。 |
継続へのデータの受け渡し
継続元への参照は、継続のユーザー デリゲートに引数として渡されます。 継続元が System.Threading.Tasks.Task<TResult> で、そのタスクが完了まで実行された場合、継続はそのタスクの Task<TResult>.Result プロパティにアクセスできます。 複数のタスクの継続と Task.WaitAll メソッドを使用している場合、引数は継続元の配列になります。 Task.WaitAny を使用した場合、引数は完了した最初の継続元になります。
Task<TResult>.Result は、タスクが完了するまでブロックします。 ただし、タスクがキャンセルまたは違反になった場合は、コードでそのタスクにアクセスしようとすると Result によって例外がスローされます。 この問題を回避するには、次の例で示すように OnlyOnRanToCompletion オプションを使用します。
Dim aTask = Task(Of Integer).Factory.StartNew(Function()
Return 54
End Function)
Dim bTask = aTask.ContinueWith(Sub(antecedent)
Console.WriteLine("continuation {0}", antecedent.Result)
End Sub,
TaskContinuationOptions.OnlyOnRanToCompletion)
var t = Task<int>.Factory.StartNew(() => 54);
var c = t.ContinueWith((antecedent) =>
{
Console.WriteLine("continuation {0}", antecedent.Result);
},
TaskContinuationOptions.OnlyOnRanToCompletion);
継続元が完了まで実行されなかった場合でも継続を実行する場合は、例外を防ぐ必要があります。 考えられる 1 つの方法として、継続元の状態をテストし、状態が Faulted または Canceled でない場合にのみ Result へのアクセスを試みるようにします。 また、継続元の Exception プロパティを調べることもできます。 詳細については、「例外処理 (タスク並列ライブラリ)」を参照してください。
継続のキャンセル
継続が Canceled 状態になるのは、次の場合です。
キャンセル要求への応答として OperationCanceledException をスローした場合。 他のタスクと同様に、例外には継続に渡されたのと同じトークンが含まれ、連携によるキャンセルの受信確認として扱われます。
System.Threading.CancellationToken が引数として継続に渡された場合、トークンの IsCancellationRequested プロパティは、継続が実行される前に true (True) になります。 このような場合、継続は開始されずに、直接 Canceled 状態に遷移します。
継続の TaskContinuationOptions の条件が満たされないために、継続が実行されない場合。 たとえば、タスクが Faulted 状態になった場合、NotOnFaulted オプションで作成された継続は Canceled 状態に遷移し、実行されません。
継続元が取り消された場合に継続を実行しないようにするには、継続を作成するときに NotOnCanceled オプションを指定します。
タスクとその継続が同じ論理的操作の 2 つの部分を表す場合は、次の例に示すように、同じキャンセル トークンを両方のタスクに渡すことができます。
Dim someCondition As Boolean = True
Dim cts As New CancellationTokenSource
Dim task1 = New Task(Sub()
Dim ct As CancellationToken = cts.Token
While someCondition = True
ct.ThrowIfCancellationRequested()
' Do the work here...
' ...
End While
End Sub,
cts.Token
)
Dim task2 = task1.ContinueWith(Sub(antecedent)
Dim ct As CancellationToken = cts.Token
While someCondition = True
ct.ThrowIfCancellationRequested()
' Do the work here
' ...
End While
End Sub,
cts.Token)
task1.Start()
' ...
' Antecedent and/or continuation will
' respond to this request, depending on when it is made.
cts.Cancel()
Task task = new Task(() =>
{
CancellationToken ct = cts.Token;
while (someCondition)
{
ct.ThrowIfCancellationRequested();
// Do the work.
//...
}
},
cts.Token
);
Task task2 = task.ContinueWith((antecedent) =>
{
CancellationToken ct = cts.Token;
while (someCondition)
{
ct.ThrowIfCancellationRequested();
// Do the work.
//...
}
},
cts.Token);
task.Start();
//...
// Antecedent and/or continuation will
// respond to this request, depending on when it is made.
cts.Cancel();
前のタスクが取り消されない場合は、継続を取り消すためにそのトークンを引き続き使用できます。 前のタスクが取り消されなかった場合、継続は開始されません。
継続が Canceled 状態になると、それらの継続に指定された TaskContinuationOptions によっては後続の継続に影響を与える場合があります。
破棄された継続は開始されません。
継続タスクと子タスク
継続は、継続元とアタッチされたすべての子タスクが完了するまでは実行されません。 継続は、デタッチされた子タスクの完了は待機しません。 継続元の最終的な状態は、アタッチされた子タスクの最終的な状態に依存します。 デタッチされた子タスクの状態は、親には影響しません。 詳細については、「入れ子のタスクと子タスク」を参照してください。
継続からスローされた例外の処理
継続元と継続の関係は、親と子の関係とは異なります。 継続によってスローされた例外は継続元へは反映されません。 したがって、継続によってスローされた例外は、他のタスクでの処理と同様、次のように処理します。
- Wait メソッド、WaitAll メソッドまたは WaitAny メソッドを使用するか、ジェネリック メソッドを使用して、継続での待機を指定します。 次の例に示すように、継続元とその継続は、同じ try (Visual Basic では Try) ステートメントで待機できます。
Dim task1 = Task(Of Integer).Factory.StartNew(Function()
Return 54
End Function)
Dim continuation = task1.ContinueWith(Sub(antecedent)
Console.WriteLine("continuation {0}", antecedent.Result)
Throw New InvalidOperationException()
End Sub)
Try
task1.Wait()
continuation.Wait()
Catch ae As AggregateException
For Each ex In ae.InnerExceptions
Console.WriteLine(ex.Message)
Next
End Try
Console.WriteLine("Exception handled. Let's move on.")
var t = Task<int>.Factory.StartNew(() => 54);
var c = t.ContinueWith((antecedent) =>
{
Console.WriteLine("continuation {0}", antecedent.Result);
throw new InvalidOperationException();
});
try
{
t.Wait();
c.Wait();
}
catch (AggregateException ae)
{
foreach(var e in ae.InnerExceptions)
Console.WriteLine(e.Message);
}
Console.WriteLine("Exception handled. Let's move on.");
最初の継続の Exception プロパティを確認するには、2 番目の継続を使用します。 詳細については、「例外処理 (タスク並列ライブラリ)」および「方法: タスクがスローした例外を処理する」を参照してください。
継続が子タスクで、AttachedToParent オプションを使用して作成された場合、アタッチされているその他の子と同様、その例外は親によって呼び出し元のスレッドに反映されます。 詳細については、「入れ子のタスクと子タスク」を参照してください。
参照
概念
履歴の変更
日付 |
履歴 |
理由 |
---|---|---|
2010 年 6 月 |
継続の非同期動作に関する説明を追加。 |
カスタマー フィードバック |