入れ子のタスクと子タスク
入れ子のタスクとは、単に他のタスクのユーザー デリゲートで作成された Task インスタンスのことです。 子タスクとは、AttachedToParent オプションで作成された入れ子のタスクのことです。 タスクでは、システム リソースが許す限り、任意の子タスクまたは入れ子のタスク、あるいはその両方を作成できます。 次の例は、単純な入れ子のタスクを 1 つ作成する親タスクを示しています。
Shared Sub SimpleNestedTask()
Dim parent = Task.Factory.StartNew(Sub()
Console.WriteLine("Outer task executing.")
Dim child = Task.Factory.StartNew(Sub()
Console.WriteLine("Nested task starting.")
Thread.SpinWait(500000)
Console.WriteLine("Nested task completing.")
End Sub)
End Sub)
parent.Wait()
Console.WriteLine("Outer task has completed.")
End Sub
' Sample output:
' Outer task executing.
' Nested task starting.
' Outer task has completed.
' Nested task completing.
static void SimpleNestedTask()
{
var parent = Task.Factory.StartNew(() =>
{
Console.WriteLine("Outer task executing.");
var child = Task.Factory.StartNew(() =>
{
Console.WriteLine("Nested task starting.");
Thread.SpinWait(500000);
Console.WriteLine("Nested task completing.");
});
});
parent.Wait();
Console.WriteLine("Outer has completed.");
}
/* Sample output:
Outer task executing.
Nested task starting.
Outer has completed.
Nested task completing.
*/
アタッチされた子タスクとデタッチされた入れ子のタスク
子タスクと入れ子のタスクに関して最も重要なことは、 入れ子のタスクは、本質的に、親タスクまたは外側のタスクからは独立しており、アタッチされた子タスクは、親と緊密に同期しているということです。 AttachedToParent オプションを使用するようにタスクの作成ステートメントを変更する場合は、次に示す例のようになります。
Dim child = Task.Factory.StartNew(Sub()
Console.WriteLine("Attached child starting.")
Thread.SpinWait(5000000)
Console.WriteLine("Attached child completing.")
End Sub, TaskCreationOptions.AttachedToParent)
var child = Task.Factory.StartNew((t) =>
{
Console.WriteLine("Attached child starting.");
Thread.SpinWait(5000000);
Console.WriteLine("Attached child completing.");
}, TaskCreationOptions.AttachedToParent);
次の出力が生成されます。
' Parent task executing.
' Attached child starting.
' Attached child completing.
' Parent has completed.
Parent task executing.
Attached child starting.
Attached child completing.
Parent has completed.
アタッチされた子タスクを使用すると、非同期操作の厳密に同期されたグラフを作成できます。 ただし、ほとんどの場合、入れ子のタスクを使用することをお勧めします。他のタスクとの関係は複雑度が低いためです。 こうした理由から、既定では他のタスク内に作成されたタスクは入れ子になっており、子タスクを作成する場合は AttachedToParent オプションを明示的に指定する必要があります。
以下の表に、2 種類の子タスクの基本的な相違点を示します。
カテゴリ |
入れ子のタスク |
アタッチされた子タスク |
---|---|---|
外側のタスク (親) は内側のタスクの完了を待機します。 |
X |
○ |
親は子 (内側のタスク) によってスローされた例外を反映します。 |
X |
○ |
親 (外側のタスク) の状態は、子 (内側のタスク) の状態に依存します。 |
X |
○ |
入れ子のタスクが Task<TResult> であるデタッチされたシナリオでは、入れ子のタスクの Result プロパティにアクセスすることで、子の待機を親に強制できます。 Result プロパティは、タスクが完了するまでブロックされます。
Shared Sub WaitForSimpleNestedTask()
Dim parent = Task(Of Integer).Factory.StartNew(Function()
Console.WriteLine("Outer task executing.")
Dim child = Task(Of Integer).Factory.StartNew(Function()
Console.WriteLine("Nested task starting.")
Thread.SpinWait(5000000)
Console.WriteLine("Nested task completing.")
Return 42
End Function)
Return child.Result
End Function)
Console.WriteLine("Outer has returned {0}", parent.Result)
End Sub
'Sample output:
' Outer task executing.
' Nested task starting.
' Detached task completing.
' Outer has returned 42
static void WaitForSimpleNestedTask()
{
var outer = Task<int>.Factory.StartNew(() =>
{
Console.WriteLine("Outer task executing.");
var nested = Task<int>.Factory.StartNew(() =>
{
Console.WriteLine("Nested task starting.");
Thread.SpinWait(5000000);
Console.WriteLine("Nested task completing.");
return 42;
});
// Parent will wait for this detached child.
return nested.Result;
});
Console.WriteLine("Outer has returned {0}.", outer.Result);
}
/* Sample output:
Outer task executing.
Nested task starting.
Nested task completing.
Outer has returned 42.
*/
入れ子のタスクと子タスクの例外
入れ子のタスクが例外をスローする場合、入れ子でないタスクの場合と同様に監視するか、または外側のタスク内で直接処理する必要があります。 アタッチされた子が例外をスローした場合、例外は自動的に親タスクに反映され、タスクの Result プロパティへのアクセスを待機するか、アクセスを試みるスレッドに戻されます。 したがって、アタッチされた子タスクを使用することで、1 つの場所ですべての例外を処理できます。つまり、呼び出し元のスレッドで Wait を呼び出すことができるということです。 詳細については、「例外処理 (タスク並列ライブラリ)」を参照してください。
キャンセルと子タスク
タスクのキャンセル処理は他の処理と連携して行われることに注意してください。 したがって、"キャンセル可能" であるためには、すべてのアタッチされた子タスク、またはデタッチされた子タスクが、キャンセル トークンの状態を監視する必要があります。 1 つのキャンセル要求を使用して親とその子をすべて取り消す場合は、同じトークンをすべてのタスクに引数として渡し、各タスクにその要求に応答するためのロジックを提供します。 詳細については、「タスクのキャンセル」および「方法: タスクとその子を取り消す」を参照してください。
親が取り消された場合
子が開始される前に親が取り消された場合、子 (入れ子の) タスクはもちろん開始されません。 子または入れ子のタスクが既に開始された後に親が取り消された場合、入れ子 (子) のタスクはそれ自体にキャンセル ロジックが適用されていない限り、完了まで実行されます。 詳細については、「タスクのキャンセル」を参照してください。
入れ子のタスクが取り消された場合
デタッチされた子タスクが、そのタスクに渡されたのと同じトークンを使用して取り消された場合、親は子を待機せず、例外も反映されません。例外は、他の処理と連携したキャセル処理として扱われるためです。 この動作は最上位のタスクと同じです。
子タスクが取り消された場合
アタッチされた子タスクが、そのタスクに渡されたのと同じトークンを使用して取り消された場合、TaskCanceledException が AggregateException 内の連結されたスレッドに反映されます。 アタッチされた子タスクのグラフにまで反映されるすべてのエラーが発生している例外に加え、問題のないすべての例外も処理できるようにするには、親タスクでの待機が非常に重要です。
詳細については、「例外処理 (タスク並列ライブラリ)」を参照してください。