Присоединенные и отсоединенные дочерние задачи

Дочерняя задача (или вложенная задача) — это экземпляр System.Threading.Tasks.Task, создаваемый в пользовательском делегате другой задачи, которая называется родительской задачей. Дочерняя задача может быть отсоединенной или присоединенной. Отсоединенная дочерняя задача — это задача, которая выполняется независимо от своего родительского объекта. Присоединенная дочерняя задача — это вложенная задача, созданная с параметром TaskCreationOptions.AttachedToParent, родительский объект которой не запрещает ее присоединение явно или по умолчанию. Задача может создавать любое количество присоединенных и отсоединенных дочерних задач, ограничиваемое только системными ресурсами.

В следующей таблице перечислены основные различия между двумя видами дочерних задач.

Категория Отсоединенные дочерние задачи Присоединенные дочерние задачи
Родительский объект ожидает завершение дочерних задач. No Да
Родительский объект распространяет исключения, созданные дочерними задачами. No Да
Состояние родительского объекта зависит от состояния дочернего объекта. No Да

В большинстве случаев рекомендуется использовать отсоединенные дочерние задачи, поскольку их связи с другими задачами менее сложные. Именно поэтому задачи, создаваемые в родительских задачах, по умолчанию отсоединены, и чтобы создать присоединенную дочернюю задачу, необходимо явно указать параметр 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("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. Это показано в следующем примере. Он аналогичен предыдущему примеру, за исключением того, что родительская задача создается путем вызова метода Task.Run(Action) вместо метода TaskFactory.StartNew(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 в вызывающем потоке. Дополнительные сведения см. в разделе Обработка исключений.

Отмена и дочерние задачи

Отмена задачи выполняется совместно. А именно, чтобы можно было отменить задачу, каждая присоединенная или отсоединенная дочерняя задача должна отслеживать состояние токена отмены. Если нужно отменить родительскую задачу и все ее дочерние задачи с помощью одного запроса отмены, передайте один и тот же токен в качестве аргумента во все задачи и предоставьте в каждую задачу логику для ответа на запрос в каждой задаче. Дополнительные сведения см. в разделах Отмена задач и Практическое руководство. Отмена задачи и ее дочерних элементов.

Когда родительская задача отменяется

Если родительская задача отменяет сама себя до запуска ее дочерней задачи, то дочерняя задача никогда не запускается. Если родительская задача отменяет сама себя после запуска ее дочерней задачи, то дочерняя задача выполняется до завершения, если в ней отсутствует собственная логика отмены. Для получения дополнительной информации см. Task Cancellation.

Когда отменяется отсоединенная дочерняя задача

Если отсоединенная дочерняя задача отменяет сама себя, используя тот же токен, переданный в родительскую задачу, и родительская задача не ожидает дочернюю задачу, то исключение не распространяется, поскольку оно трактуется как неопасная совместная отмена. Это происходит так же, как и в случае любой задачи верхнего уровня.

Когда отменяется присоединенная дочерняя задача

Если присоединенная дочерняя задача отменяет саму себя, используя тот же токен, который был передан в ее родительскую задачу, TaskCanceledException распространяется в присоединенный поток в объекте AggregateException. Вы должны ожидать родительскую задачу, чтобы можно было обработать все неопасные исключения в дополнение ко всем исключениям с ошибкой, которые распространяются вверх по графу присоединенных дочерних задач.

Дополнительные сведения см. в разделе Обработка исключений.

Запрет присоединения дочерней задачи к ее родителю

Необработанное исключение, вызванное дочерней задачей, распространяется в родительскую задачу. Вы можете использовать это поведение, чтобы наблюдать за всеми исключениями дочерних задач в одной корневой задаче, вместо того чтобы перемещаться по дереву задач. Однако распространение исключений может привести к проблемам, если родительская задача не ожидает присоединение из другого кода. Например, рассмотрим приложение, которое вызывает сторонний компонент библиотеки из объекта Task. Если сторонний компонент библиотеки также создает объект Task и задает TaskCreationOptions.AttachedToParent, чтобы присоединить его к родительской задаче, то все необработанные исключения, возникающие в дочерней задаче, распространяются в родительскую задачу. Это может привести к непредвиденному поведению в основном приложении.

Чтобы запретить присоединение к родительской задаче дочерних задач, укажите параметр TaskCreationOptions.DenyChildAttach при создании родительской задачи Task или объекта Task<TResult>. Если задача пытается присоединиться к родительской, а родительская задача задана с параметром TaskCreationOptions.DenyChildAttach, то дочерней задаче не удастся присоединиться к родительской задаче, и она будет выполняться в точности так, как если бы параметр TaskCreationOptions.AttachedToParent не был указан.

Можно также запрещать присоединение дочерней задачи к ее родителю, когда дочерняя задача не завершается своевременно. Поскольку родительская задача не завершается, пока не будут завершены все ее дочерние задачи, дочерняя задача с длительным временем выполнения может привести к снижению производительности всего приложения. Пример, в котором показано, как повысить производительность приложения, предотвращая присоединение задачи к ее родительской задаче, см. в руководстве по запрету присоединения дочерней задачи к ее родительской задаче.

См. также