Поделиться через


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

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

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

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

В большинстве случаев рекомендуется использовать отсоединяемые дочерние задачи, так как их отношения с другими задачами являются менее сложными. Именно поэтому задачи, созданные внутри родительских задач, отсоединяются по умолчанию, и необходимо явно указать параметр TaskCreationOptions.AttachedToParent для создания присоединенной дочерней задачи.

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

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

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<TResult>, а не объектом Task, можно обеспечить, что родительская задача будет ожидать завершения дочерней, получив доступ к свойству 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.

Можно использовать вложенные дочерние задачи для создания строго синхронизированных графов выполнения асинхронных операций.

Однако дочерняя задача может присоединяться к родительской задаче только в том случае, если её родитель не запрещает присоединение дочерних задач. Родительские задачи могут явным образом предотвратить присоединение к ним дочерних задач, указав параметр 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 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 в вызывающем потоке. Дополнительные сведения см. в разделе Обработка исключений.

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

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

Когда родитель отменяет

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

При отмене отсоединяемой дочерней задачи

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

При отмене присоединенной дочерней задачи

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

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

Предотвращение присоединения дочерней задачи к родительской задаче

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

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

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

См. также