Tareas secundarias asociadas y desasociadas

Una tarea secundaria o tarea anidada es una instancia de System.Threading.Tasks.Task que se crea en el delegado de usuario de otra tarea, conocida como tarea primaria. Una tarea secundaria puede estar desasociada o asociada. Una tarea secundaria desasociada es una tarea que se ejecuta independientemente de su elemento principal. Una tarea secundaria asociada es una tarea anidada que se crea con la opción TaskCreationOptions.AttachedToParent y cuyo elemento primario no le prohíbe asociarse de forma explícita o predeterminada. Una tarea puede crear cualquier número de tareas secundarias asociadas y desasociadas, con la única limitación de los recursos del sistema.

En la tabla siguiente se muestran las diferencias básicas entre los dos tipos de tareas secundarias.

Category Tareas secundarias desasociadas Tareas secundarias asociadas
La tarea primaria espera a que se completen las tareas secundarias. No
La tarea primaria propaga las excepciones que producen las tareas secundarias. No
El estado de la tarea primaria depende del estado de la tarea secundaria. No

En la mayoría de los casos, se recomienda usar tareas secundarias desasociadas porque las relaciones con otras tareas son menos complejas. Esta es la razón por la que las tareas que se crean dentro de tareas primarias están desasociadas de forma predeterminada y es necesario especificar explícitamente la opción TaskCreationOptions.AttachedToParent para crear una tarea secundaria asociada.

Tareas secundarias desasociadas

Aunque una tarea secundaria se crea mediante una tarea primaria, de forma predeterminada es independiente de la tarea primaria. En el ejemplo siguiente se muestra una tarea primaria que crea una tarea secundaria simple. Si se ejecuta varias veces el código de ejemplo, se observa que el resultado del ejemplo se diferencia del que se muestra, y también que el resultado puede cambiar cada vez que se ejecuta el código. Esto ocurre porque la tarea primaria y las tareas secundarias se ejecutan de forma independiente; la tarea secundaria es una tarea independiente. El ejemplo únicamente espera que se complete la tarea primaria; la tarea secundaria puede no ejecutarse o completarse antes de que finalice la aplicación de consola.

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.

Si la tarea secundaria se representa mediante un objeto Task<TResult> en lugar de un objeto Task, puede garantizarse que la tarea primaria esperará a que la tarea secundaria se complete teniendo acceso a la propiedad Task<TResult>.Result de la tarea secundaria, aunque sea una tarea secundaria desasociada. La propiedad Result se bloquea hasta que se completa su tarea, como se muestra en el ejemplo siguiente.

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

Tareas secundarias asociadas

A diferencia de las tareas secundarias desasociadas, las tareas secundarias asociadas se sincronizan estrechamente con la tarea primaria. En el ejemplo anterior, se puede cambiar la tarea secundaria desasociada a una tarea secundaria asociada mediante la opción TaskCreationOptions.AttachedToParent en la instrucción de creación de tareas, como se muestra en el ejemplo siguiente. En este código, la tarea secundaria asociada se completa antes que su tarea primaria. Como resultado, el resultado del ejemplo es igual cada vez que se ejecuta el 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.

Puede usar tareas secundarias asociadas para crear gráficos con una estrecha sincronización de operaciones asincrónicas.

Sin embargo, una tarea secundaria se puede asociar a su elemento primario solo si su elemento primario no prohíbe las tareas secundarias asociadas. Las tareas primarias pueden evitar explícitamente que las tareas secundarias se asocien a ellas especificando la opción TaskCreationOptions.DenyChildAttach del constructor de clase de la tarea primaria o el método TaskFactory.StartNew. Las tareas primarias impiden implícitamente que las tareas secundarias se asocien a ellas si se crean mediante una llamada al método Task.Run. Esto se ilustra en el siguiente ejemplo: Es idéntico al ejemplo anterior, excepto en que la tarea primaria se crea mediante una llamada al método Task.Run(Action) en lugar de al método TaskFactory.StartNew(Action). Como la tarea secundaria no se puede asociar a su elemento primario, el resultado del ejemplo es impredecible. Como las opciones de creación de tareas predeterminadas para las sobrecargas de Task.Run incluyen TaskCreationOptions.DenyChildAttach, este ejemplo es funcionalmente equivalente al primer ejemplo de la sección "Tareas secundarias desasociadas".

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.

Excepciones en tareas secundarias

Si una tarea secundaria desasociadas inicia una excepción, esa excepción debe observarse o controlarse directamente en la tarea primaria como si se tratara de una tarea no anidada. Si una tarea secundaria asociada inicia una excepción, la excepción se propaga automáticamente a la tarea primaria y de nuevo al subproceso, que espera o intenta obtener acceso a la propiedad Task<TResult>.Result de la tarea. Por tanto, si se usan tareas secundarias asociadas, se pueden controlar todas las excepciones en un solo punto en la llamada a Task.Wait del subproceso que realiza la llamada. Para más información, consulte Control de excepciones.

Cancelación y tareas secundarias

La cancelación de tareas es cooperativa. Es decir, para que se pueda cancelar, cada tarea secundaria asociada o desasociada debe supervisar el estado del token de cancelación. Si desea cancelar un elemento primario y todos sus elementos secundarios utilizando una solicitud de cancelación, debe pasar el mismo token como argumento a todas las tareas y proporcionar en cada tarea la lógica de respuesta a la solicitud en cada tarea. Para más información, vea Cancelación de tareas y Cómo: Cancelar una tarea y sus elementos secundarios.

Cuando la tarea primaria se cancela

Si una tarea primaria se cancela antes de que se inicie su tarea secundaria, la tarea secundaria nunca se inicia. Si una tarea primaria se cancela después de que se ha iniciado su tarea secundaria, la tarea secundaria se ejecutará hasta completarse a menos que tenga su propia lógica de cancelación. Para más información, vea Task Cancellation.

Cuando una tarea secundaria desasociada se cancela

Si una tarea secundaria desasociada se cancela usando el mismo token que se pasó a la tarea primaria, y la tarea primaria no espera a la tarea secundaria, no se propagará ninguna excepción puesto que la excepción se trata cono una cancelación de cooperación benigna. Este comportamiento es igual que el de cualquier tarea de nivel superior.

Cuando se cancela una tarea secundaria asociada

Cuando una tarea secundaria asociada se cancela usando el mismo token que se pasó a su tarea primaria, se propaga una excepción TaskCanceledException al subproceso de unión dentro de AggregateException. Se debe esperar a la tarea primaria para poder controlar todas las excepciones benignas además de todas las excepciones de error que se propagan de manera ascendente a través de un gráfico de tareas secundarias asociadas.

Para más información, consulte Control de excepciones.

Impedir que una tarea secundaria se adjunte a su tarea primara

Una excepción no controlada producida por una tarea secundaria se propaga a la tarea primaria. Puede usar este comportamiento para observar todas las excepciones de tareas secundarias desde una tarea raíz en lugar de recorrer un árbol de tareas. Sin embargo, la propagación de excepciones puede dar problemas cuando una tarea primaria no cuenta con datos adjuntos de otro código. Por ejemplo, piense en una aplicación que llama a un componente de la biblioteca de terceros de un objeto Task. Si el componente de la biblioteca de terceros también crea un objeto Task y especifica TaskCreationOptions.AttachedToParent para asociarlo a la tarea primaria, las excepciones no controladas que aparecen en la tarea secundaria se propagan a la tarea primaria. Esto podría dar lugar a un comportamiento inesperado en la aplicación principal.

Para evitar que una tarea secundaria se adjunte a su tarea primaria, especifique la opción TaskCreationOptions.DenyChildAttach cuando cree el objeto primario Task o Task<TResult>. Cuando una tarea intenta asociarse a su elemento primario y el elemento primario especifica la opción TaskCreationOptions.DenyChildAttach, la tarea secundaria no podrá asociarse a un elemento primario y se ejecutará como si no se hubiera especificado la opción TaskCreationOptions.AttachedToParent.

Puede que también desee evitar que una tarea secundaria se adjunte a su tarea primaria cuando la tarea secundaria no finaliza a tiempo. Dado que una tarea primaria no finaliza hasta que finalizan todas las tareas secundarias, una tarea secundaria que se ejecute durante mucho tiempo puede provocar que el rendimiento general de la aplicación sea mediocre. Para obtener un ejemplo que muestra cómo mejorar el rendimiento de la aplicación impidiendo que una tarea se asocie a su tarea primaria, consulte Cómo: Evitar que una tarea secundaria se asocie a su elemento primario.

Vea también