Tarefas filho anexadas e desanexadas

Uma tarefa filho (ou tarefa aninhada) é uma instância System.Threading.Tasks.Task que é criada no representante do usuário de outra tarefa, conhecida como a tarefa pai. Uma tarefa filho pode ser desanexada ou anexada. Um tarefa filho desanexada é uma tarefa que é executada de forma independente de sua tarefa pai. Uma tarefa filho anexada é uma tarefa aninhada criada com a opção TaskCreationOptions.AttachedToParent cuja tarefa pai não explicitamente ou por padrão proíbe-a de ser anexada. Uma tarefa pode criar qualquer número de tarefas filho anexadas e desanexadas que só são limitadas pelos recursos do sistema.

A tabela a seguir lista as diferenças básicas entre os dois tipos de tarefas filho.

Categoria Tarefas filho desanexadas Tarefas filho anexadas
A tarefa pai aguarda a conclusão de tarefas filho. No Sim
A tarefa pai propaga exceções lançadas pelas tarefas filho. No Sim
O status da tarefa pai depende do status da tarefa filho. No Sim

Na maioria dos cenários, é recomendável usar tarefas filho desanexadas porque suas relações com outras tarefas são menos complexas. É por isso que as tarefas criadas dentro de tarefas pai são desanexadas por padrão e você deve especificar explicitamente a opção TaskCreationOptions.AttachedToParent para criar uma tarefa filho anexada.

Tarefas filho desanexadas

Embora uma tarefa filho seja criada por uma tarefa pai, por padrão ela é independente da tarefa pai. No exemplo a seguir, uma tarefa pai cria uma tarefa filho simples. Se você executar o exemplo de código várias vezes, perceberá que a saída do exemplo é diferente daquilo que é exibido e, também, que a saída pode mudar sempre que o código for executado. Isso ocorre porque as tarefas pai e filho são executadas de forma independente e a tarefa filho é uma tarefa desanexada. O exemplo espera apenas que a tarefa pai seja concluída e a tarefa filho pode não ser executada ou concluída antes do término do aplicativo de console.

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.

Se a tarefa filho é representada por um objeto Task<TResult> em vez de um objeto Task, é possível garantir que a tarefa pai aguardará a conclusão da tarefa filho acessando a propriedade Task<TResult>.Result da tarefa filho, mesmo que ela seja uma tarefa filho desanexada. A propriedade Result é bloqueada até a tarefa ser concluída, conforme mostra o exemplo a seguir.

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

Tarefas filho anexadas

Ao contrário das tarefas filho desanexadas, as tarefas filho anexadas são estreitamente sincronizadas com as tarefas pai. Altere a tarefa filho desanexada do exemplo anterior para uma tarefa filho anexada usando a opção TaskCreationOptions.AttachedToParent na instrução de criação da tarefa, conforme mostrado no exemplo a seguir. Nesse código, a tarefa filho anexada é concluída antes de sua tarefa pai. Como resultado, a saída do exemplo é a mesma sempre que você executar o código.

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.

Você pode usar tarefas filho anexadas para criar gráficos totalmente sincronizados de operações assíncronas.

No entanto, uma tarefa filho pode se anexada à tarefa pai somente se a tarefa pai não proibir tarefas filho anexadas. As tarefas pai podem explicitamente impedir que tarefas filho se anexem a elas especificando a opção TaskCreationOptions.DenyChildAttach no constructo de classe da tarefa pai ou no método TaskFactory.StartNew. As tarefas pai podem explicitamente impedir que as tarefas filho se anexem a elas se elas tiverem sido criadas chamando o método Task.Run. O exemplo a seguir ilustra essa situação. Ele é idêntico ao exemplo anterior, exceto que a tarefa pai é criada chamando o método Task.Run(Action) em vez do método TaskFactory.StartNew(Action). Como a tarefa filho não consegue se anexar à sua tarefa pai, a saída do exemplo é imprevisível. Como as opções de criação da tarefa padrão para as sobrecargas Task.Run incluem TaskCreationOptions.DenyChildAttach, este exemplo é funcionalmente equivalente ao primeiro exemplo na seção "Tarefas filho desanexadas".

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.

Exceções em tarefas filho

Se uma tarefa filho desanexada lançar uma exceção, essa exceção deverá ser observada ou manipulada diretamente na tarefa pai assim como ocorre com todas as tarefas não aninhadas. Se uma tarefa filho anexada lançar uma exceção, a exceção será propagada automaticamente para a tarefa pai e de volta para o thread que espera ou tenta acessar a propriedade Task<TResult>.Result da tarefa. Portanto, usando tarefas filho anexadas, você pode tratar de todas as exceções em apenas um ponto na chamada para Task.Wait no thread da chamada. Para saber mais, veja Tratamento de exceção.

Cancelamento e tarefas filho

O cancelamento de tarefas é cooperativo. Ou seja, para ser anulável, todas as tarefas filho anexadas ou desanexadas devem monitorar o status do token de cancelamento. Se quiser cancelar uma tarefa pai e todas as tarefas filho usando uma solicitação de cancelamento, passe o mesmo token como um argumento para todas as tarefas e forneça em cada tarefa a lógica para responder à solicitação de cada tarefa. Para obter mais informações, consulte Cancelamento de tarefas e Como cancelar uma tarefa e seus filhos.

Quando a tarefa pai é cancelada

Se houver o cancelamento de uma tarefa pai antes do início de sua tarefa filho, a tarefa filho nunca será iniciada. Se houver o cancelamento de uma tarefa pai após sua tarefa filho já tiver sido iniciada, a tarefa filho será executada até ser concluída, a não ser que ela tenha sua própria lógica de cancelamento. Para obter mais informações, consulte Cancelamento de tarefas.

Quando uma tarefa filho desanexada é cancelada

Se houver o cancelamento de uma tarefa filho desanexada usando o mesmo token que foi passado para a tarefa pai e a tarefa pai não aguardar a tarefa filho, nenhuma exceção será propagada já que a exceção é tratada como um cancelamento de cooperação benigno. Esse comportamento é igual em todas as tarefas de nível superior.

Quando uma tarefa filho anexada é cancelada

Quando há o cancelamento de uma tarefa filho anexada usando o mesmo token que foi passado para sua tarefa pai, uma TaskCanceledException é propagada para o thread de junção dentro de um AggregateException. Você deve aguardar a tarefa pai para tratar de todas as exceções benignas, além de todas as exceções de falha que são propagadas em um gráfico de tarefas filho anexadas.

Para saber mais, veja Tratamento de exceção.

Impedir uma tarefa filho de se anexar à sua tarefa pai

Uma exceção sem tratamento que é gerada por uma tarefa filho é propagada para a tarefa pai. Você pode usar esse comportamento para observar todas as exceções da tarefa filho de uma tarefa raiz em vez de percorrer uma árvore de tarefas. No entanto, a propagação da exceção pode ser problemática quando uma tarefa pai não espera anexos de outro código. Por exemplo, considere um aplicativo que chama um componente de biblioteca de terceiros de um objeto Task. Se o componente da biblioteca de terceiros também criar um objeto Task e especificar TaskCreationOptions.AttachedToParent para anexá-lo à tarefa pai, todas as exceções sem tratamento que ocorrem na tarefa filho serão propagadas para o pai. Isso pode resultar em comportamento inesperado no aplicativo principal.

Para impedir que uma tarefa filho seja anexada a uma tarefa pai, especifique a opção TaskCreationOptions.DenyChildAttach ao criar a Task pai ou o objeto Task<TResult>. Quando uma tarefa tenta se anexar à sua tarefa pai e a tarefa pai especifica a opção TaskCreationOptions.DenyChildAttach, a tarefa filho não poderá se anexar a uma tarefa pai e será executada apenas como se a opção TaskCreationOptions.AttachedToParent não fosse especificada.

É provável que você também queira impedir que uma tarefa filho se anexe à sua tarefa pai quando a tarefa filho não for concluída de maneira oportuna. Como a tarefa pai não termina até que todas as tarefas filho sejam concluídas, uma tarefa filho de longa duração pode fazer com que o aplicativo geral tenha um baixo desempenho. Para obter um exemplo que mostre como melhorar o desempenho do aplicativo impedindo que ele seja anexado à sua tarefa pai, confira Como evitar que uma tarefa filho se anexe à sua tarefa pai.

Confira também