アタッチされた子タスクとデタッチされた子タスク

子タスク (または入れ子のタスク) は、親タスク と呼ばれる、別のタスクのユーザー デリゲートで作成された、System.Threading.Tasks.Task のインスタンスです。 子タスクはデタッチまたはアタッチできます。 デタッチされた子タスク は、親とは独立して実行されるタスクです。 アタッチされた子タスク は、TaskCreationOptions.AttachedToParent オプションで作成される入れ子のタスクです。その親は、明示的にも既定でも、子タスクがアタッチされることを禁止しません。 タスクでは、システム リソースが許す限り、任意の数のアタッチされた子タスクおよびデタッチされた子タスクを作成できます。

以下の表に、2 種類の子タスクの基本的な相違点を示します。

カテゴリ デタッチされた子タスク アタッチされた子タスク
親は子タスクが完了するまで待機します。 いいえ はい
親は子タスクによってスローされた例外を反映します。 いいえ はい
親のステータスは子のステータスに依存します。 いいえ はい

ほとんどの場合、デタッチされた子タスクを使用することをお勧めします。他のタスクとの関係は複雑度が低いためです。 こうした理由から、既定では親タスク内に作成されたタスクはデタッチされており、アタッチされた子タスクを作成する場合は TaskCreationOptions.AttachedToParent オプションを明示的に指定する必要があります。

デタッチされた子タスク

子タスクは親タスクによって作成されますが、既定では親タスクに依存しません。 次の例では、親タスクが単に 1 つの子タスクを作成します。 このコード例を複数回実行すると、出力がここに示したものとは異なり、またコードを実行するたびに出力が変わる場合があることに気付くことがあります。 これは親タスクと子タスクが、それぞれ独立して実行されるために生じます。子タスクはデタッチされたタスクです。 この例は親タスクの完了のみを待機します。コンソール アプリが終了する前には、子タスクは実行または完了しないことがあります。

using System;
using System.Threading;
using System.Threading.Tasks;

public class Example
{
   public static void Main()
   {
      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.");
   }
}
// The example produces output like the following:
//        Outer task executing.
//        Nested task starting.
//        Outer has completed.
//        Nested task completing.
Imports System.Threading
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        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
End Module
' The example produces output like the following:
'   Outer task executing.
'   Nested task starting.
'   Outer task has completed.
'   Nested task completing.

子タスクが Task<TResult> オブジェクトではなく Task オブジェクトによって表される場合、デタッチされた子タスクであっても、子の Task<TResult>.Result プロパティにアクセスして、親タスクが子タスクの完了を待機することを確認できます。 次の例に示すように、Result プロパティはタスクが完了するまでブロックします。

using System;
using System.Threading;
using System.Threading.Tasks;

class Example
{
   static void Main()
   {
      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);
   }
}
// The example displays the following output:
//       Outer task executing.
//       Nested task starting.
//       Nested task completing.
//       Outer has returned 42.
Imports System.Threading
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        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
End Module
' The example displays the following output:
'       Outer task executing.
'       Nested task starting.
'       Detached task completing.
'       Outer has returned 42

アタッチされた子タスク

デタッチされた子タスクとは異なり、アタッチされた子タスクは親と緊密に同期します。 次の例に示すように、タスクの作成ステートメントで TaskCreationOptions.AttachedToParent オプションを使用すると、前の例のデタッチされた子タスクを、アタッチされた子タスクに変更できます。 このコードでは、アタッチされた子タスクは親の前に完了します。 その結果、この例の出力結果は、コードを実行するたびに同じになります。

using System;
using System.Threading;
using System.Threading.Tasks;

public class Example
{
   public static void Main()
   {
      var parent = Task.Factory.StartNew(() => {
            Console.WriteLine("Parent task executing.");
            var child = Task.Factory.StartNew(() => {
                  Console.WriteLine("Attached child starting.");
                  Thread.SpinWait(5000000);
                  Console.WriteLine("Attached child completing.");
            }, TaskCreationOptions.AttachedToParent);
      });
      parent.Wait();
      Console.WriteLine("Parent has completed.");
   }
}
// The example displays the following output:
//       Parent task executing.
//       Attached child starting.
//       Attached child completing.
//       Parent has completed.
Imports System.Threading
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim parent = Task.Factory.StartNew(Sub()
                                               Console.WriteLine("Parent task executing")
                                               Dim child = Task.Factory.StartNew(Sub()
                                                                                     Console.WriteLine("Attached child starting.")
                                                                                     Thread.SpinWait(5000000)
                                                                                     Console.WriteLine("Attached child completing.")
                                                                                 End Sub, TaskCreationOptions.AttachedToParent)
                                           End Sub)
        parent.Wait()
        Console.WriteLine("Parent has completed.")
    End Sub
End Module
' The example displays the following output:
'       Parent task executing.
'       Attached child starting.
'       Attached child completing.
'       Parent has completed.

アタッチされた子タスクを使用すると、非同期操作の厳密に同期されたグラフを作成できます。

ただし、その親タスクが子タスクのアタッチを禁止していない場合にのみ、子タスクは親タスクにアタッチできます。 親タスクは、親タスクのクラスのコンストラクターの TaskCreationOptions.DenyChildAttach オプションまたは TaskFactory.StartNew メソッドで指定することで、明示的に子タスクが親タスクにアタッチできないようにすることができます。 親タスクが Task.Run メソッドを呼び出して作成された場合、親タスクは暗黙的に子タスクをアタッチできないようにします。 次の例を使って説明します。 親タスクが TaskFactory.StartNew(Action) メソッドではなく Task.Run(Action) メソッドを呼び出して作成される点を除き、これは前の例と同一です。 子タスクはその親にアタッチすることができないため、例からの出力は予測できません。 Task.Run のオーバーロードにおける既定のタスクの作成オプションには TaskCreationOptions.DenyChildAttach が含まれるため、この例は、「デタッチされた子タスク」セクションの最初の例と機能的に同等です。

using System;
using System.Threading;
using System.Threading.Tasks;

public class Example
{
   public static void Main()
   {
      var parent = Task.Run(() => {
            Console.WriteLine("Parent task executing.");
            var child = Task.Factory.StartNew(() => {
                  Console.WriteLine("Attached child starting.");
                  Thread.SpinWait(5000000);
                  Console.WriteLine("Attached child completing.");
            }, TaskCreationOptions.AttachedToParent);
      });
      parent.Wait();
      Console.WriteLine("Parent has completed.");
   }
}
// The example displays output like the following:
//       Parent task executing.
//       Parent has completed.
//       Attached child starting.
Imports System.Threading
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim parent = Task.Run(Sub()
                                  Console.WriteLine("Parent task executing.")
                                  Dim child = Task.Factory.StartNew(Sub()
                                                                        Console.WriteLine("Attached child starting.")
                                                                        Thread.SpinWait(5000000)
                                                                        Console.WriteLine("Attached child completing.")
                                                                    End Sub, TaskCreationOptions.AttachedToParent)
                              End Sub)
        parent.Wait()
        Console.WriteLine("Parent has completed.")
    End Sub
End Module
' The example displays output like the following:
'       Parent task executing.
'       Parent has completed.
'       Attached child starting.

子タスクでの例外

デタッチされた子タスクが例外をスローする場合、その例外は入れ子でないタスクの場合と同様に監視するか、または親タスク内で直接処理する必要があります。 アタッチされた子タスクが例外をスローした場合、例外は自動的に親タスクに反映され、タスクの Task<TResult>.Result プロパティへのアクセスを待機するか、アクセスを試みるスレッドに戻されます。 したがって、アタッチされた子タスクを使用することで、呼び出し元のスレッドの Task.Wait の呼び出しの 1 つの場所ですべての例外を処理できます。 詳細については、「例外処理」を参照してください。

キャンセルと子タスク

タスクの取り消し処理は他の処理と連携して行われます。 つまり、キャンセル可能であるためには、すべてのアタッチされた子タスク、またはデタッチされた子タスクが、キャンセル トークンの状態を監視する必要があります。 1 つのキャンセル要求を使用して親とその子をすべて取り消す場合は、同じトークンをすべてのタスクに引数として渡し、各タスクの要求に応答するためのロジックを各タスクに提供します。 詳細については、「タスクのキャンセル」と「方法:タスクとその子を取り消す」を参照してください。

親が取り消された場合

子タスクが開始される前に親が取り消された場合、子は開始されません。 子タスクが既に開始された後に親が取り消された場合、子はそれ自体にキャンセル ロジックが適用されていない限り、完了まで実行されます。 詳細については、「タスクのキャンセル」をご覧ください。

デタッチされた子タスクが取り消された場合

デタッチされた子タスクが、そのタ親に渡されたのと同じトークンを使用して取り消された場合、親は子タスクを待機せず、例外も反映されません。例外は、他の処理と連携したキャセル処理として扱われるためです。 この動作は最上位のタスクと同じです。

アタッチされた子タスクが取り消された場合

アタッチされた子タスクが、その親タスクに渡されたのと同じトークンを使用して取り消された場合、TaskCanceledExceptionAggregateException 内の連結されたスレッドに反映されます。 アタッチされた子タスクのグラフにまで反映されるすべてのエラーが発生している例外に加え、問題のないすべての例外も処理できるようにするため、親タスクを待機する必要があります。

詳細については、「例外処理」を参照してください。

子タスクがその親にアタッチされないようにする

子タスクがスローした未処理の例外は親タスクに反映されます。 この動作を使うと、タスク ツリーを走査することなく、1 つのルート タスクのすべての子タスクの例外を確認することができます。 ただし、親タスクは他のコードからのアタッチを想定していない場合には、例外の反映は問題となる場合があります。 たとえば、Task オブジェクトのサードパーティ ライブラリのコンポーネントを呼び出すアプリケーションを考えてみます。 サードパーティのライブラリのコンポーネントが Task オブジェクトを作成し、親タスクにアタッチするように TaskCreationOptions.AttachedToParent を指定する場合は、子タスクで発生するハンドルされない例外はすべて親に反映されます。 これによりメイン アプリケーションで予期しない動作が発生することがあります。

子タスクが親タスクにアタッチされないようにするには、親の TaskCreationOptions.DenyChildAttach または Task オブジェクトを作成するときに、Task<TResult> オプションを指定します。 タスクがその親にアタッチしようとし、親が TaskCreationOptions.DenyChildAttach オプションを指定する場合、子タスクは親にアタッチされず、TaskCreationOptions.AttachedToParent オプションが指定されなかったかのように実行されます。

子タスクが適時に完了しない場合には、子タスクがその親にアタッチしないようにすることをお勧めします。 親タスクは、すべての子タスクが終了するまで完了しないため、長時間実行される子タスクによって、アプリケーション全体のパフォーマンスの低下を生じる場合があります。 タスクがその親タスクにアタッチしないようにすることにより、アプリケーションのパフォーマンスを向上させる方法の例については、「方法: 子タスクがその親にアタッチしないようにする」を参照してください。

関連項目