次の方法で共有


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

子タスク (または入れ子になったタスク) は、タスクと呼ばれる別のタスクのユーザー デリゲートに作成されるSystem.Threading.Tasks.Task インスタンスです。 子タスクは、デタッチすることもアタッチすることもできます。 デタッチされた子タスクは、親とは別に実行されるタスクです。 アタッチされた子タスクは、TaskCreationOptions.AttachedToParent オプションを使用して作成される入れ子になったタスクで、親が明示的にアタッチしないか、既定でアタッチを禁止します。 タスクは、アタッチされた子タスクとデタッチされた子タスクをいくつでも作成でき、システム リソースによってのみ制限されます。

次の表に、2 種類の子タスクの基本的な違いを示します。

カテゴリ デタッチされた子タスク アタッチされた子タスク
親は子タスクの完了を待機します。 いいえ イエス
親は、子タスクによってスローされた例外を伝達します。 いいえ イエス
親の状態は、子の状態によって異なります。 いいえ イエス

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

デタッチされた子タスク

子タスクは親タスクによって作成されますが、既定では親タスクとは独立しています。 次の例では、親タスクによって 1 つの単純な子タスクが作成されます。 コード例を複数回実行すると、例からの出力が示した出力と異なっており、コードを実行するたびに出力が変更される場合があります。 これは、親タスクと子タスクが互いに独立して実行されるために発生します。子はデタッチされたタスクです。 この例では、親タスクの完了のみを待機し、コンソール アプリが終了する前に子タスクが実行または完了しない場合があります。

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

public class Example4
{
    public static void Main()
    {
        Task parent = Task.Factory.StartNew(() =>
        {
            Console.WriteLine("Outer task executing.");

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

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

class Example3
{
    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 {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.

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

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

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

public class Example2
{
   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 を指定すると、子タスクで発生する未処理の例外が親に伝達されます。 これにより、メイン アプリで予期しない動作が発生する可能性があります。

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

また、子タスクがタイムリーに終了しない場合に、子タスクが親にアタッチされないようにすることもできます。 親タスクは、すべての子タスクが完了するまで終了しないため、実行時間の長い子タスクにより、アプリ全体のパフォーマンスが低下する可能性があります。 タスクが親タスクにアタッチされないようにしてアプリのパフォーマンスを向上させる方法を示す例については、「 方法: 子タスクが親にアタッチされないようにする」を参照してください。

こちらも参照ください