Encadenamiento de tareas mediante tareas de continuación

En la programación asincrónica, es habitual que una operación asincrónica, al finalizar, invoque una segunda operación. Las continuaciones permiten a las operaciones descendentes usar los resultados de la primera operación. Tradicionalmente, las continuaciones se han realizado mediante métodos de devolución de llamada. En la biblioteca TPL, se proporciona la misma función mediante tareas de continuación. Una tarea de continuación (también conocida simplemente como una continuación) es una tarea asincrónica invocada por otra tarea, conocida como antecedente, cuando esta finaliza.

A pesar de que las continuaciones son relativamente fáciles de usar, resultan eficaces y flexibles. Por ejemplo, se puede:

  • Pasar datos del antecedente a la continuación.
  • Especificar las condiciones precisas en las que se invoca o no se invoca la continuación.
  • Cancelar una continuación antes de que se inicie o de forma cooperativa mientras se ejecuta.
  • Proporcionar sugerencias sobre cómo debería programarse la continuación.
  • Invocar varias continuaciones desde el mismo antecedente.
  • Invocar una continuación cuando todos o alguno de los antecedentes finalicen.
  • Encadenar las continuaciones una tras otra hasta cualquier longitud arbitraria.
  • Usar una continuación para controlar las excepciones producidas por el antecedente.

Sobre las continuaciones

Una continuación es una tarea que se crea en el estado WaitingForActivation. Se activa automáticamente cuando su tarea (o tareas) antecedente finaliza. Llamar a Task.Start en una continuación en código de usuario produce una excepción System.InvalidOperationException .

Una continuación es en sí misma un elemento Task y no bloquea el subproceso en el que se inicia. Para un bloqueo, llame al método Task.Wait hasta que finalice la tarea de continuación.

Creación de una continuación para un solo antecedente

Se puede crear una continuación que se ejecute una vez completado su antecedente mediante una llamada al método Task.ContinueWith . En el ejemplo siguiente se muestra el patrón básico (para mayor claridad, se omite el control de excepciones). En él se ejecuta una tarea antecedente, taskA, que devuelve un objeto DayOfWeek que indica el nombre del día actual de la semana. Cuando taskA se completa, antecedent representa sus resultados en el método de continuación ContinueWith. El resultado de la tarea antecedente se escribe en la consola.

using System;
using System.Threading.Tasks;

public class SimpleExample
{
    public static async Task Main()
    {
        // Declare, assign, and start the antecedent task.
        Task<DayOfWeek> taskA = Task.Run(() => DateTime.Today.DayOfWeek);

        // Execute the continuation when the antecedent finishes.
        await taskA.ContinueWith(antecedent => Console.WriteLine($"Today is {antecedent.Result}."));
    }
}
// The example displays the following output:
//       Today is Monday.
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        ' Execute the antecedent.
        Dim taskA As Task(Of DayOfWeek) = Task.Run(Function() DateTime.Today.DayOfWeek)

        ' Execute the continuation when the antecedent finishes.
        Dim continuation As Task = taskA.ContinueWith(Sub(antecedent)
                                                          Console.WriteLine("Today is {0}.", antecedent.Result)
                                                      End Sub)
        continuation.Wait()
    End Sub
End Module
' The example displays output like the following output:
'       Today is Monday.

Creación de una continuación para varios antecedentes

También puede crear una continuación que se ejecutará cuando se haya completado una tarea o un grupo de tareas. Para ejecutar una continuación cuando se hayan completado todas las tareas antecedentes, puede llamar al método estático Task.WhenAll (Shared en Visual Basic) o al método de instancia TaskFactory.ContinueWhenAll. Para ejecutar una continuación cuando cualquiera de las tareas antecedentes se haya completado, llame al método estático Task.WhenAny (Shared en Visual Basic) o al método de instancia TaskFactory.ContinueWhenAny.

Las llamadas a las sobrecargas Task.WhenAll y Task.WhenAny no bloquean el subproceso que realiza la llamada. Sin embargo, se suele llamar a todos menos a los métodos Task.WhenAll(IEnumerable<Task>) y Task.WhenAll(Task[]) para recuperar la propiedad Task<TResult>.Result devuelta, que bloquea el subproceso que realiza la llamada.

En el ejemplo siguiente se llama al método Task.WhenAll(IEnumerable<Task>) para crear una tarea de continuación que refleje los resultados de sus diez tareas antecedentes. Cada tarea antecedente eleva al cuadrado un valor de índice que varía entre uno y diez. Si los antecedentes se completan correctamente (su propiedad Task.Status es TaskStatus.RanToCompletion), la propiedad Task<TResult>.Result de la continuación es una matriz de los valores Task<TResult>.Result devueltos por cada antecedente. En el ejemplo se suman para calcular el total de los cuadrados de todos los números entre 1 y 10:

using System.Collections.Generic;
using System;
using System.Threading.Tasks;

public class WhenAllExample
{
    public static async Task Main()
    {
        var tasks = new List<Task<int>>();
        for (int ctr = 1; ctr <= 10; ctr++)
        {
            int baseValue = ctr;
            tasks.Add(Task.Factory.StartNew(b => (int)b! * (int)b, baseValue));
        }

        var results = await Task.WhenAll(tasks);

        int sum = 0;
        for (int ctr = 0; ctr <= results.Length - 1; ctr++)
        {
            var result = results[ctr];
            Console.Write($"{result} {((ctr == results.Length - 1) ? "=" : "+")} ");
            sum += result;
        }

        Console.WriteLine(sum);
    }
}
// The example displays the similar output:
//    1 + 4 + 9 + 16 + 25 + 36 + 49 + 64 + 81 + 100 = 385
Imports System.Collections.Generic
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim tasks As New List(Of Task(Of Integer))()
        For ctr As Integer = 1 To 10
            Dim baseValue As Integer = ctr
            tasks.Add(Task.Factory.StartNew(Function(b)
                                                Dim i As Integer = CInt(b)
                                                Return i * i
                                            End Function, baseValue))
        Next
        Dim continuation = Task.WhenAll(tasks)

        Dim sum As Long = 0
        For ctr As Integer = 0 To continuation.Result.Length - 1
            Console.Write("{0} {1} ", continuation.Result(ctr),
                          If(ctr = continuation.Result.Length - 1, "=", "+"))
            sum += continuation.Result(ctr)
        Next
        Console.WriteLine(sum)
    End Sub
End Module
' The example displays the following output:
'       1 + 4 + 9 + 16 + 25 + 36 + 49 + 64 + 81 + 100 = 385

Opciones de continuación

Cuando se crea una continuación de tarea única, se puede usar una sobrecarga ContinueWith que toma un valor de enumeración System.Threading.Tasks.TaskContinuationOptions para especificar las condiciones en las que se inicia la continuación. Por ejemplo, se puede especificar que la continuación únicamente se ejecute si el antecedente se completa correctamente, o solo si se completa en un estado de error. Si la condición no es true cuando el antecedente está listo para invocar la continuación, la continuación realiza la transición directamente al estado TaskStatus.Canceled y no se puede iniciar posteriormente.

Hay muchos métodos de continuación de varias tareas, como las sobrecargas del método TaskFactory.ContinueWhenAll, que también incluyen un parámetro System.Threading.Tasks.TaskContinuationOptions. Pero solo es válido un subconjunto de todos los miembros de enumeración de System.Threading.Tasks.TaskContinuationOptions. Se pueden especificar valores System.Threading.Tasks.TaskContinuationOptions que tengan equivalentes en la enumeración System.Threading.Tasks.TaskCreationOptions , como TaskContinuationOptions.AttachedToParent, TaskContinuationOptions.LongRunningy TaskContinuationOptions.PreferFairness. Si se especifica cualquiera de las opciones NotOn o OnlyOn con una continuación de varias tareas, se producirá una excepción ArgumentOutOfRangeException en tiempo de ejecución.

Para obtener más información sobre las opciones de continuación de tareas, vea el artículo TaskContinuationOptions.

Paso de datos a una continuación

El método Task.ContinueWith pasa una referencia al antecedente al delegado de usuario de la continuación como argumento. Si el antecedente es un objeto System.Threading.Tasks.Task<TResult> y la tarea se ejecutó hasta que se completó, la continuación puede luego tener acceso a la propiedad Task<TResult>.Result de la tarea.

La propiedad Task<TResult>.Result se bloquea hasta que se completa la tarea. Sin embargo, si la tarea se canceló o produjo errores y se intenta tener acceso a la propiedad Result , se producirá una excepción AggregateException . Puede evitar este problema con la opción OnlyOnRanToCompletion, tal como se muestra en el ejemplo siguiente:

using System;
using System.Threading.Tasks;

public class ResultExample
{
    public static async Task Main()
    {
       var task = Task.Run(
           () =>
           {
                DateTime date = DateTime.Now;
                return date.Hour > 17
                    ? "evening"
                    : date.Hour > 12
                        ? "afternoon"
                        : "morning";
            });
        
        await task.ContinueWith(
            antecedent =>
            {
                Console.WriteLine($"Good {antecedent.Result}!");
                Console.WriteLine($"And how are you this fine {antecedent.Result}?");
            }, TaskContinuationOptions.OnlyOnRanToCompletion);
   }
}
// The example displays the similar output:
//       Good afternoon!
//       And how are you this fine afternoon?
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim t = Task.Run(Function()
                             Dim dat As DateTime = DateTime.Now
                             If dat = DateTime.MinValue Then
                                 Throw New ArgumentException("The clock is not working.")
                             End If

                             If dat.Hour > 17 Then
                                 Return "evening"
                             Else If dat.Hour > 12 Then
                                 Return "afternoon"
                             Else
                                 Return "morning"
                             End If
                         End Function)
        Dim c = t.ContinueWith(Sub(antecedent)
                                   Console.WriteLine("Good {0}!",
                                                     antecedent.Result)
                                   Console.WriteLine("And how are you this fine {0}?",
                                                     antecedent.Result)
                               End Sub, TaskContinuationOptions.OnlyOnRanToCompletion)
        c.Wait()
    End Sub
End Module
' The example displays output like the following:
'       Good afternoon!
'       And how are you this fine afternoon?

Si quiere que la continuación se ejecute aunque el antecedente no haya finalizado correctamente, debe protegerse de la excepción. Un modo de hacerlo es probar la propiedad Task.Status del antecedente y solo intentar el acceso a la propiedad Result si el estado no es Faulted ni Canceled. También puede examinar la propiedad Exception del antecedente. Para más información, consulte Control de excepciones. En el ejemplo siguiente se modifica el ejemplo anterior para tener acceso a la propiedad Task<TResult>.Result del antecedente, pero solo si su estado es TaskStatus.RanToCompletion:

using System;
using System.Threading.Tasks;

public class ResultTwoExample
{
    public static async Task Main() =>
        await Task.Run(
            () =>
            {
                DateTime date = DateTime.Now;
                return date.Hour > 17
                   ? "evening"
                   : date.Hour > 12
                       ? "afternoon"
                       : "morning";
            })
            .ContinueWith(
                antecedent =>
                {
                    if (antecedent.Status == TaskStatus.RanToCompletion)
                    {
                        Console.WriteLine($"Good {antecedent.Result}!");
                        Console.WriteLine($"And how are you this fine {antecedent.Result}?");
                    }
                    else if (antecedent.Status == TaskStatus.Faulted)
                    {
                        Console.WriteLine(antecedent.Exception!.GetBaseException().Message);
                    }
                });
}
// The example displays output like the following:
//       Good afternoon!
//       And how are you this fine afternoon?
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim t = Task.Run(Function()
                             Dim dat As DateTime = DateTime.Now
                             If dat = DateTime.MinValue Then
                                 Throw New ArgumentException("The clock is not working.")
                             End If

                             If dat.Hour > 17 Then
                                 Return "evening"
                             Else If dat.Hour > 12 Then
                                 Return "afternoon"
                             Else
                                 Return "morning"
                             End If
                         End Function)
        Dim c = t.ContinueWith(Sub(antecedent)
                                   If t.Status = TaskStatus.RanToCompletion Then
                                       Console.WriteLine("Good {0}!",
                                                         antecedent.Result)
                                       Console.WriteLine("And how are you this fine {0}?",
                                                         antecedent.Result)
                                   Else If t.Status = TaskStatus.Faulted Then
                                       Console.WriteLine(t.Exception.GetBaseException().Message)
                                   End If
                               End Sub)
    End Sub
End Module
' The example displays output like the following:
'       Good afternoon!
'       And how are you this fine afternoon?

Cancelación de una continuación

La propiedad Task.Status de una continuación se establece en TaskStatus.Canceled en las situaciones siguientes:

Si una tarea y su continuación representan dos partes de la misma operación lógica, se puede pasar el mismo token de cancelación a ambas tareas, tal como se muestra en el ejemplo siguiente. Consta de un antecedente que genera una lista de enteros divisibles por 33 y que pasa a la continuación. La continuación a su vez muestra la lista. El antecedente y la continuación se ponen en pausa periódicamente durante intervalos aleatorios. Además, un objeto System.Threading.Timer se usa para ejecutar el método Elapsed después de un intervalo de tiempo de espera de cinco segundos. Este ejemplo llama al método CancellationTokenSource.Cancel, que hace que la tarea que se ejecuta actualmente llame al método CancellationToken.ThrowIfCancellationRequested. Que el método CancellationTokenSource.Cancel se invoque cuando se está ejecutando el antecedente o su continuación depende de la duración de las pausas generadas de forma aleatoria. Si se cancela el antecedente, la continuación no se iniciará. Si no se cancela el antecedente, el token aún puede usarse para cancelar la continuación.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

public class CancellationExample
{
    static readonly Random s_random = new Random((int)DateTime.Now.Ticks);

    public static async Task Main()
    {
        using var cts = new CancellationTokenSource();
        CancellationToken token = cts.Token;
        var timer = new Timer(Elapsed, cts, 5000, Timeout.Infinite);

        var task = Task.Run(
            async () =>
            {
                var product33 = new List<int>();
                for (int index = 1; index < short.MaxValue; index++)
                {
                    if (token.IsCancellationRequested)
                    {
                        Console.WriteLine("\nCancellation requested in antecedent...\n");
                        token.ThrowIfCancellationRequested();
                    }
                    if (index % 2000 == 0)
                    {
                        int delay = s_random.Next(16, 501);
                        await Task.Delay(delay);
                    }
                    if (index % 33 == 0)
                    {
                        product33.Add(index);
                    }
                }

                return product33.ToArray();
            }, token);

        Task<double> continuation = task.ContinueWith(
            async antecedent =>
            {
                Console.WriteLine("Multiples of 33:\n");
                int[] array = antecedent.Result;
                for (int index = 0; index < array.Length; index++)
                {
                    if (token.IsCancellationRequested)
                    {
                        Console.WriteLine("\nCancellation requested in continuation...\n");
                        token.ThrowIfCancellationRequested();
                    }
                    if (index % 100 == 0)
                    {
                        int delay = s_random.Next(16, 251);
                        await Task.Delay(delay);
                    }

                    Console.Write($"{array[index]:N0}{(index != array.Length - 1 ? ", " : "")}");

                    if (Console.CursorLeft >= 74)
                    {
                        Console.WriteLine();
                    }
                }
                Console.WriteLine();
                return array.Average();
            }, token).Unwrap();

        try
        {
            await task;
            double result = await continuation;
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }

        Console.WriteLine("\nAntecedent Status: {0}", task.Status);
        Console.WriteLine("Continuation Status: {0}", continuation.Status);
    }

    static void Elapsed(object? state)
    {
        if (state is CancellationTokenSource cts)
        {
            cts.Cancel();
            Console.WriteLine("\nCancellation request issued...\n");
        }
    }
}
// The example displays the similar output:
//     Multiples of 33:
//     
//     33, 66, 99, 132, 165, 198, 231, 264, 297, 330, 363, 396, 429, 462, 495, 528,
//     561, 594, 627, 660, 693, 726, 759, 792, 825, 858, 891, 924, 957, 990, 1,023,
//     1,056, 1,089, 1,122, 1,155, 1,188, 1,221, 1,254, 1,287, 1,320, 1,353, 1,386,
//     1,419, 1,452, 1,485, 1,518, 1,551, 1,584, 1,617, 1,650, 1,683, 1,716, 1,749,
//     1,782, 1,815, 1,848, 1,881, 1,914, 1,947, 1,980, 2,013, 2,046, 2,079, 2,112,
//     2,145, 2,178, 2,211, 2,244, 2,277, 2,310, 2,343, 2,376, 2,409, 2,442, 2,475,
//     2,508, 2,541, 2,574, 2,607, 2,640, 2,673, 2,706, 2,739, 2,772, 2,805, 2,838,
//     2,871, 2,904, 2,937, 2,970, 3,003, 3,036, 3,069, 3,102, 3,135, 3,168, 3,201,
//     3,234, 3,267, 3,300, 3,333, 3,366, 3,399, 3,432, 3,465, 3,498, 3,531, 3,564,
//     3,597, 3,630, 3,663, 3,696, 3,729, 3,762, 3,795, 3,828, 3,861, 3,894, 3,927,
//     3,960, 3,993, 4,026, 4,059, 4,092, 4,125, 4,158, 4,191, 4,224, 4,257, 4,290,
//     4,323, 4,356, 4,389, 4,422, 4,455, 4,488, 4,521, 4,554, 4,587, 4,620, 4,653,
//     4,686, 4,719, 4,752, 4,785, 4,818, 4,851, 4,884, 4,917, 4,950, 4,983, 5,016,
//     5,049, 5,082, 5,115, 5,148, 5,181, 5,214, 5,247, 5,280, 5,313, 5,346, 5,379,
//     5,412, 5,445, 5,478, 5,511, 5,544, 5,577, 5,610, 5,643, 5,676, 5,709, 5,742,
//     Cancellation request issued...
//
//     5,775,
//     Cancellation requested in continuation...
//       
//     The operation was canceled.
//       
//     Antecedent Status: RanToCompletion
//     Continuation Status: Canceled
Imports System.Collections.Generic
Imports System.Threading
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim rnd As New Random()
        Dim lockObj As New Object()
        Dim cts As New CancellationTokenSource()
        Dim token As CancellationToken = cts.Token
        Dim timer As New Timer(AddressOf Elapsed, cts, 5000, Timeout.Infinite)

        Dim t = Task.Run(Function()
                             Dim product33 As New List(Of Integer)()
                             For ctr As Integer = 1 To Int16.MaxValue
                                 ' Check for cancellation.
                                 If token.IsCancellationRequested Then
                                     Console.WriteLine("\nCancellation requested in antecedent...\n")
                                     token.ThrowIfCancellationRequested()
                                 End If
                                 ' Introduce a delay.
                                 If ctr Mod 2000 = 0 Then
                                     Dim delay As Integer
                                     SyncLock lockObj
                                         delay = rnd.Next(16, 501)
                                     End SyncLock
                                     Thread.Sleep(delay)
                                 End If

                                 ' Determine if this is a multiple of 33.
                                 If ctr Mod 33 = 0 Then product33.Add(ctr)
                             Next
                             Return product33.ToArray()
                         End Function, token)

        Dim continuation = t.ContinueWith(Sub(antecedent)
                                              Console.WriteLine("Multiples of 33:" + vbCrLf)
                                              Dim arr = antecedent.Result
                                              For ctr As Integer = 0 To arr.Length - 1
                                                  If token.IsCancellationRequested Then
                                                      Console.WriteLine("{0}Cancellation requested in continuation...{0}",
                                                                        vbCrLf)
                                                      token.ThrowIfCancellationRequested()
                                                  End If

                                                  If ctr Mod 100 = 0 Then
                                                      Dim delay As Integer
                                                      SyncLock lockObj
                                                          delay = rnd.Next(16, 251)
                                                      End SyncLock
                                                      Thread.Sleep(delay)
                                                  End If
                                                  Console.Write("{0:N0}{1}", arr(ctr),
                                                                If(ctr <> arr.Length - 1, ", ", ""))
                                                  If Console.CursorLeft >= 74 Then Console.WriteLine()
                                              Next
                                              Console.WriteLine()
                                          End Sub, token)

        Try
            continuation.Wait()
        Catch e As AggregateException
            For Each ie In e.InnerExceptions
                Console.WriteLine("{0}: {1}", ie.GetType().Name,
                                  ie.Message)
            Next
        Finally
            cts.Dispose()
        End Try

        Console.WriteLine(vbCrLf + "Antecedent Status: {0}", t.Status)
        Console.WriteLine("Continuation Status: {0}", continuation.Status)
    End Sub

    Private Sub Elapsed(state As Object)
        Dim cts As CancellationTokenSource = TryCast(state, CancellationTokenSource)
        If cts Is Nothing Then return

        cts.Cancel()
        Console.WriteLine("{0}Cancellation request issued...{0}", vbCrLf)
    End Sub
End Module
' The example displays output like the following:
'    Multiples of 33:
'
'    33, 66, 99, 132, 165, 198, 231, 264, 297, 330, 363, 396, 429, 462, 495, 528,
'    561, 594, 627, 660, 693, 726, 759, 792, 825, 858, 891, 924, 957, 990, 1,023,
'    1,056, 1,089, 1,122, 1,155, 1,188, 1,221, 1,254, 1,287, 1,320, 1,353, 1,386,
'    1,419, 1,452, 1,485, 1,518, 1,551, 1,584, 1,617, 1,650, 1,683, 1,716, 1,749,
'    1,782, 1,815, 1,848, 1,881, 1,914, 1,947, 1,980, 2,013, 2,046, 2,079, 2,112,
'    2,145, 2,178, 2,211, 2,244, 2,277, 2,310, 2,343, 2,376, 2,409, 2,442, 2,475,
'    2,508, 2,541, 2,574, 2,607, 2,640, 2,673, 2,706, 2,739, 2,772, 2,805, 2,838,
'    2,871, 2,904, 2,937, 2,970, 3,003, 3,036, 3,069, 3,102, 3,135, 3,168, 3,201,
'    3,234, 3,267, 3,300, 3,333, 3,366, 3,399, 3,432, 3,465, 3,498, 3,531, 3,564,
'    3,597, 3,630, 3,663, 3,696, 3,729, 3,762, 3,795, 3,828, 3,861, 3,894, 3,927,
'    3,960, 3,993, 4,026, 4,059, 4,092, 4,125, 4,158, 4,191, 4,224, 4,257, 4,290,
'    4,323, 4,356, 4,389, 4,422, 4,455, 4,488, 4,521, 4,554, 4,587, 4,620, 4,653,
'    4,686, 4,719, 4,752, 4,785, 4,818, 4,851, 4,884, 4,917, 4,950, 4,983, 5,016,
'    5,049, 5,082, 5,115, 5,148, 5,181, 5,214, 5,247, 5,280, 5,313, 5,346, 5,379,
'    5,412, 5,445, 5,478, 5,511, 5,544, 5,577, 5,610, 5,643, 5,676, 5,709, 5,742,
'    5,775, 5,808, 5,841, 5,874, 5,907, 5,940, 5,973, 6,006, 6,039, 6,072, 6,105,
'    6,138, 6,171, 6,204, 6,237, 6,270, 6,303, 6,336, 6,369, 6,402, 6,435, 6,468,
'    6,501, 6,534, 6,567, 6,600, 6,633, 6,666, 6,699, 6,732, 6,765, 6,798, 6,831,
'    6,864, 6,897, 6,930, 6,963, 6,996, 7,029, 7,062, 7,095, 7,128, 7,161, 7,194,
'    7,227, 7,260, 7,293, 7,326, 7,359, 7,392, 7,425, 7,458, 7,491, 7,524, 7,557,
'    7,590, 7,623, 7,656, 7,689, 7,722, 7,755, 7,788, 7,821, 7,854, 7,887, 7,920,
'    7,953, 7,986, 8,019, 8,052, 8,085, 8,118, 8,151, 8,184, 8,217, 8,250, 8,283,
'    8,316, 8,349, 8,382, 8,415, 8,448, 8,481, 8,514, 8,547, 8,580, 8,613, 8,646,
'    8,679, 8,712, 8,745, 8,778, 8,811, 8,844, 8,877, 8,910, 8,943, 8,976, 9,009,
'    9,042, 9,075, 9,108, 9,141, 9,174, 9,207, 9,240, 9,273, 9,306, 9,339, 9,372,
'    9,405, 9,438, 9,471, 9,504, 9,537, 9,570, 9,603, 9,636, 9,669, 9,702, 9,735,
'    9,768, 9,801, 9,834, 9,867, 9,900, 9,933, 9,966, 9,999, 10,032, 10,065, 10,098,
'    10,131, 10,164, 10,197, 10,230, 10,263, 10,296, 10,329, 10,362, 10,395, 10,428,
'    10,461, 10,494, 10,527, 10,560, 10,593, 10,626, 10,659, 10,692, 10,725, 10,758,
'    10,791, 10,824, 10,857, 10,890, 10,923, 10,956, 10,989, 11,022, 11,055, 11,088,
'    11,121, 11,154, 11,187, 11,220, 11,253, 11,286, 11,319, 11,352, 11,385, 11,418,
'    11,451, 11,484, 11,517, 11,550, 11,583, 11,616, 11,649, 11,682, 11,715, 11,748,
'    11,781, 11,814, 11,847, 11,880, 11,913, 11,946, 11,979, 12,012, 12,045, 12,078,
'    12,111, 12,144, 12,177, 12,210, 12,243, 12,276, 12,309, 12,342, 12,375, 12,408,
'    12,441, 12,474, 12,507, 12,540, 12,573, 12,606, 12,639, 12,672, 12,705, 12,738,
'    12,771, 12,804, 12,837, 12,870, 12,903, 12,936, 12,969, 13,002, 13,035, 13,068,
'    13,101, 13,134, 13,167, 13,200, 13,233, 13,266,
'    Cancellation requested in continuation...
'
'
'    Cancellation request issued...
'
'    TaskCanceledException: A task was canceled.
'
'    Antecedent Status: RanToCompletion
'    Continuation Status: Canceled

También se puede evitar que una continuación se ejecute si su antecedente se cancela sin proporcionar a la continuación un token de cancelación. Para ello, debe especificarse la opción TaskContinuationOptions.NotOnCanceled al crear la continuación, tal como se muestra en el ejemplo siguiente:

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

public class CancellationTwoExample
{
    public static async Task Main()
    {
        using var cts = new CancellationTokenSource();
        CancellationToken token = cts.Token;
        cts.Cancel();

        var task = Task.FromCanceled(token);
        Task continuation =
            task.ContinueWith(
                antecedent => Console.WriteLine("The continuation is running."),
                TaskContinuationOptions.NotOnCanceled);

        try
        {
            await task;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
            Console.WriteLine();
        }

        Console.WriteLine($"Task {task.Id}: {task.Status:G}");
        Console.WriteLine($"Task {continuation.Id}: {continuation.Status:G}");
    }
}
// The example displays the similar output:
//       TaskCanceledException: A task was canceled.
//
//       Task 1: Canceled
//       Task 2: Canceled
Imports System.Threading
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim cts As New CancellationTokenSource()
        Dim token As CancellationToken = cts.Token
        cts.Cancel()

        Dim t As Task = Task.FromCanceled(token)
        Dim continuation As Task = t.ContinueWith(Sub(antecedent)
                                                      Console.WriteLine("The continuation is running.")
                                                  End Sub, TaskContinuationOptions.NotOnCanceled)
        Try
            t.Wait()
        Catch e As AggregateException
            For Each ie In e.InnerExceptions
                Console.WriteLine("{0}: {1}", ie.GetType().Name, ie.Message)
            Next
            Console.WriteLine()
        Finally
            cts.Dispose()
        End Try

        Console.WriteLine("Task {0}: {1:G}", t.Id, t.Status)
        Console.WriteLine("Task {0}: {1:G}", continuation.Id,
                          continuation.Status)
    End Sub
End Module
' The example displays the following output:
'       TaskCanceledException: A task was canceled.
'
'       Task 1: Canceled
'       Task 2: Canceled

Cuando una continuación entra en el estado Canceled puede afectar a las continuaciones posteriores, en función de los valores de TaskContinuationOptions que se especificaron para esas continuaciones.

Las continuaciones eliminadas no se iniciarán.

Continuaciones y tareas secundarias

Una continuación no se ejecuta hasta que el antecedente y todas sus tareas secundarias asociadas no se completen. Una La continuación no espera a que las tareas secundarias desasociadas finalicen. En los dos ejemplos siguientes se ilustran tareas secundarias que se asocian y desasocian de un antecedente que crea una continuación. En el ejemplo siguiente, la continuación solo se ejecuta después de que se completen todas las tareas secundarias, y si se ejecuta el ejemplo varias veces, se genera una salida idéntica cada vez. En el ejemplo se inicia el antecedente llamando al método TaskFactory.StartNew, ya que, de manera predeterminada, el método Task.Run crea una tarea primaria cuya opción de creación de tarea predeterminada es TaskCreationOptions.DenyChildAttach.

using System;
using System.Threading.Tasks;

public class AttachedExample
{
    public static async Task Main()
    {
        await Task.Factory
                  .StartNew(
            () =>
            {
                Console.WriteLine($"Running antecedent task {Task.CurrentId}...");
                Console.WriteLine("Launching attached child tasks...");
                for (int ctr = 1; ctr <= 5; ctr++)
                {
                    int index = ctr;
                    Task.Factory.StartNew(async value =>
                    {
                        Console.WriteLine($"   Attached child task #{value} running");
                        await Task.Delay(1000);
                    }, index, TaskCreationOptions.AttachedToParent);
                }
                Console.WriteLine("Finished launching attached child tasks...");
            }).ContinueWith(
                antecedent =>
                    Console.WriteLine($"Executing continuation of Task {antecedent.Id}"));
    }
}
// The example displays the similar output:
//     Running antecedent task 1...
//     Launching attached child tasks...
//     Finished launching attached child tasks...
//        Attached child task #1 running
//        Attached child task #5 running
//        Attached child task #3 running
//        Attached child task #2 running
//        Attached child task #4 running
//     Executing continuation of Task 1
Imports System.Threading
Imports System.Threading.Tasks

Public Module Example
    Public Sub Main()
        Dim t = Task.Factory.StartNew(Sub()
                                          Console.WriteLine("Running antecedent task {0}...",
                                                            Task.CurrentId)
                                          Console.WriteLine("Launching attached child tasks...")
                                          For ctr As Integer = 1 To 5
                                              Dim index As Integer = ctr
                                              Task.Factory.StartNew(Sub(value)
                                                                        Console.WriteLine("   Attached child task #{0} running",
                                                                                          value)
                                                                        Thread.Sleep(1000)
                                                                    End Sub, index, TaskCreationOptions.AttachedToParent)
                                          Next
                                          Console.WriteLine("Finished launching attached child tasks...")
                                      End Sub)
        Dim continuation = t.ContinueWith(Sub(antecedent)
                                              Console.WriteLine("Executing continuation of Task {0}",
                                                                antecedent.Id)
                                          End Sub)
        continuation.Wait()
    End Sub
End Module
' The example displays the following output:
'       Running antecedent task 1...
'       Launching attached child tasks...
'       Finished launching attached child tasks...
'          Attached child task #5 running
'          Attached child task #1 running
'          Attached child task #2 running
'          Attached child task #3 running
'          Attached child task #4 running
'       Executing continuation of Task 1

No obstante, si las tareas secundarias se desasocian del antecedente, la continuación se ejecuta en cuanto finaliza el antecedente y con independencia del estado de las tareas secundarias. Como resultado, varias ejecuciones del ejemplo siguiente pueden tener una salida distinta que depende de cómo el programador de tareas controla cada tarea secundaria:

using System;
using System.Threading.Tasks;

public class DetachedExample
{
    public static async Task Main()
    {
        Task task =
            Task.Factory.StartNew(
                () =>
                {
                    Console.WriteLine($"Running antecedent task {Task.CurrentId}...");
                    Console.WriteLine("Launching attached child tasks...");
                    for (int ctr = 1; ctr <= 5; ctr++)
                    {
                        int index = ctr;
                        Task.Factory.StartNew(
                            async value =>
                            {
                                Console.WriteLine($"   Attached child task #{value} running");
                                await Task.Delay(1000);
                            }, index);
                    }
                    Console.WriteLine("Finished launching detached child tasks...");
                }, TaskCreationOptions.DenyChildAttach);

        Task continuation =
            task.ContinueWith(
                antecedent =>
                    Console.WriteLine($"Executing continuation of Task {antecedent.Id}"));

        await continuation;

        Console.ReadLine();
    }
}
// The example displays the similar output:
//     Running antecedent task 1...
//     Launching attached child tasks...
//     Finished launching detached child tasks...
//     Executing continuation of Task 1
//        Attached child task #1 running
//        Attached child task #5 running
//        Attached child task #2 running
//        Attached child task #3 running
//        Attached child task #4 running
Imports System.Threading
Imports System.Threading.Tasks

Public Module Example
    Public Sub Main()
        Dim t = Task.Factory.StartNew(Sub()
                                          Console.WriteLine("Running antecedent task {0}...",
                                                            Task.CurrentId)
                                          Console.WriteLine("Launching attached child tasks...")
                                          For ctr As Integer = 1 To 5
                                              Dim index As Integer = ctr
                                              Task.Factory.StartNew(Sub(value)
                                                                        Console.WriteLine("   Attached child task #{0} running",
                                                                                          value)
                                                                        Thread.Sleep(1000)
                                                                    End Sub, index)
                                          Next
                                          Console.WriteLine("Finished launching detached child tasks...")
                                      End Sub, TaskCreationOptions.DenyChildAttach)
        Dim continuation = t.ContinueWith(Sub(antecedent)
                                              Console.WriteLine("Executing continuation of Task {0}",
                                                                antecedent.Id)
                                          End Sub)
        continuation.Wait()
    End Sub
End Module
' The example displays output like the following:
'       Running antecedent task 1...
'       Launching attached child tasks...
'       Finished launching detached child tasks...
'          Attached child task #1 running
'          Attached child task #2 running
'          Attached child task #5 running
'          Attached child task #3 running
'       Executing continuation of Task 1
'          Attached child task #4 running

El estado final de la tarea antecedente depende del estado final de las tareas secundarias asociadas. El estado de las tareas secundarias desasociadas no afecta al elemento primario. Para más información, consulte Tareas secundarias asociadas y desasociadas.

Asociación de estado con continuaciones

Un estado arbitrario se puede asociar con una continuación de tarea. El método ContinueWith proporciona versiones sobrecargadas, y cada una de ellas toma un valor Object que representa el estado de la continuación. Más adelante se puede tener acceso a este objeto de estado mediante la propiedad Task.AsyncState . Si no se proporciona un valor, este objeto de estado es null.

El estado de continuación es útil al convertir un código existente que use el modelo de programación asincrónica (APM) para utilizar la TPL. En el APM, puede proporcionar el estado del objeto en el método BeginMethod y, posteriormente, puede usar la propiedad IAsyncResult.AsyncState para acceder a ese estado. Si se usa el método ContinueWith, se puede conservar este estado al convertir código que usa el APM para usar la TPL.

El estado de continuación también puede ser útil cuando se trabaja con objetos Task en el depurador de Visual Studio. Por ejemplo, en la ventana Tareas paralelas , la columna Tarea muestra la representación de cadena del objeto de estado de cada tarea. Para más información sobre la ventana Tareas paralelas, consulte el artículo Usar la ventana Tareas.

En el ejemplo siguiente se muestra cómo usar el estado de continuación. En él se crea una cadena de tareas de continuación. Cada tarea proporciona la hora actual —un objeto DateTime — para el parámetro state del método ContinueWith . Cada objeto DateTime representa la hora en que se creó la tarea de continuación. Cada tarea produce como resultado un segundo objeto DateTime que representa la hora en que finaliza la tarea. Una vez que finalizan todas las tareas, se muestran la hora de creación y la hora de finalización de cada tarea de continuación.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

class ContinuationStateExample
{
    static DateTime DoWork()
    {
        Thread.Sleep(2000);

        return DateTime.Now;
    }

    static async Task Main()
    {
        Task<DateTime> task = Task.Run(() => DoWork());

        var continuations = new List<Task<DateTime>>();
        for (int i = 0; i < 5; i++)
        {
            task = task.ContinueWith((antecedent, _) => DoWork(), DateTime.Now);
            continuations.Add(task);
        }

        await task;

        foreach (Task<DateTime> continuation in continuations)
        {
            DateTime start = (DateTime)continuation.AsyncState!;
            DateTime end = continuation.Result;

            Console.WriteLine($"Task was created at {start.TimeOfDay} and finished at {end.TimeOfDay}.");
        }

        Console.ReadLine();
    }
}
// The example displays the similar output:
//     Task was created at 10:56:21.1561762 and finished at 10:56:25.1672062.
//     Task was created at 10:56:21.1610677 and finished at 10:56:27.1707646.
//     Task was created at 10:56:21.1610677 and finished at 10:56:29.1743230.
//     Task was created at 10:56:21.1610677 and finished at 10:56:31.1779883.
//     Task was created at 10:56:21.1610677 and finished at 10:56:33.1837083.
Imports System.Collections.Generic
Imports System.Threading
Imports System.Threading.Tasks

' Demonstrates how to associate state with task continuations.
Public Module ContinuationState
    ' Simulates a lengthy operation and returns the time at which
    ' the operation completed.
    Public Function DoWork() As Date
        ' Simulate work by suspending the current thread
        ' for two seconds.
        Thread.Sleep(2000)

        ' Return the current time.
        Return Date.Now
    End Function

    Public Sub Main()
        ' Start a root task that performs work.
        Dim t As Task(Of Date) = Task(Of Date).Run(Function() DoWork())

        ' Create a chain of continuation tasks, where each task is
        ' followed by another task that performs work.
        Dim continuations As New List(Of Task(Of DateTime))()
        For i As Integer = 0 To 4
            ' Provide the current time as the state of the continuation.
            t = t.ContinueWith(Function(antecedent, state) DoWork(), DateTime.Now)
            continuations.Add(t)
        Next

        ' Wait for the last task in the chain to complete.
        t.Wait()

        ' Display the creation time of each continuation (the state object)
        ' and the completion time (the result of that task) to the console.
        For Each continuation In continuations
            Dim start As DateTime = CDate(continuation.AsyncState)
            Dim [end] As DateTime = continuation.Result

            Console.WriteLine("Task was created at {0} and finished at {1}.",
               start.TimeOfDay, [end].TimeOfDay)
        Next
    End Sub
End Module
' The example displays output like the following:
'       Task was created at 10:56:21.1561762 and finished at 10:56:25.1672062.
'       Task was created at 10:56:21.1610677 and finished at 10:56:27.1707646.
'       Task was created at 10:56:21.1610677 and finished at 10:56:29.1743230.
'       Task was created at 10:56:21.1610677 and finished at 10:56:31.1779883.
'       Task was created at 10:56:21.1610677 and finished at 10:56:33.1837083.

Continuaciones que devuelven tipos de tareas

En ocasiones, puede que deba encadenar una continuación que devuelve un tipo Task. Estas tareas se conocen como tareas anidadas. Cuando una tarea primaria llama a Task<TResult>.ContinueWith y proporciona un elemento continuationFunction que devuelve tareas, puede llamar a Unwrap para crear una tarea del proxy que representa la operación asincrónica de <Task<Task<T>>> o Task(Of Task(Of T)) (Visual Basic).

En el ejemplo siguiente se muestra cómo usar continuaciones que encapsulan funciones adicionales que devuelven tareas. Cada continuación se puede desencapsular, de modo que se expone la tarea interna que se encapsuló.

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

public class UnwrapExample
{
    public static async Task Main()
    {
        Task<int> taskOne = RemoteIncrement(0);
        Console.WriteLine("Started RemoteIncrement(0)");

        Task<int> taskTwo = RemoteIncrement(4)
            .ContinueWith(t => RemoteIncrement(t.Result))
            .Unwrap().ContinueWith(t => RemoteIncrement(t.Result))
            .Unwrap().ContinueWith(t => RemoteIncrement(t.Result))
            .Unwrap();

        Console.WriteLine("Started RemoteIncrement(...(RemoteIncrement(RemoteIncrement(4))...)");

        try
        {
            await taskOne;
            Console.WriteLine("Finished RemoteIncrement(0)");

            await taskTwo;
            Console.WriteLine("Finished RemoteIncrement(...(RemoteIncrement(RemoteIncrement(4))...)");
        }
        catch (Exception e)
        {
            Console.WriteLine($"A task has thrown the following (unexpected) exception:\n{e}");
        }
    }

    static Task<int> RemoteIncrement(int number) =>
        Task<int>.Factory.StartNew(
            obj =>
            {
                Thread.Sleep(1000);

                int x = (int)(obj!);
                Console.WriteLine("Thread={0}, Next={1}", Thread.CurrentThread.ManagedThreadId, ++x);
                return x;
            },
            number);
}

// The example displays the similar output:
//     Started RemoteIncrement(0)
//     Started RemoteIncrement(...(RemoteIncrement(RemoteIncrement(4))...)
//     Thread=4, Next=1
//     Finished RemoteIncrement(0)
//     Thread=5, Next=5
//     Thread=6, Next=6
//     Thread=6, Next=7
//     Thread=6, Next=8
//     Finished RemoteIncrement(...(RemoteIncrement(RemoteIncrement(4))...)
Imports System.Threading

Module UnwrapExample
    Sub Main()
        Dim taskOne As Task(Of Integer) = RemoteIncrement(0)
        Console.WriteLine("Started RemoteIncrement(0)")

        Dim taskTwo As Task(Of Integer) = RemoteIncrement(4).
            ContinueWith(Function(t) RemoteIncrement(t.Result)).
            Unwrap().ContinueWith(Function(t) RemoteIncrement(t.Result)).
            Unwrap().ContinueWith(Function(t) RemoteIncrement(t.Result)).
            Unwrap()

        Console.WriteLine("Started RemoteIncrement(...(RemoteIncrement(RemoteIncrement(4))...)")

        Try
            taskOne.Wait()
            Console.WriteLine("Finished RemoteIncrement(0)")

            taskTwo.Wait()
            Console.WriteLine("Finished RemoteIncrement(...(RemoteIncrement(RemoteIncrement(4))...)")
        Catch e As AggregateException
            Console.WriteLine($"A task has thrown the following (unexpected) exception:{vbLf}{e}")
        End Try
    End Sub

    Function RemoteIncrement(ByVal number As Integer) As Task(Of Integer)
        Return Task(Of Integer).Factory.StartNew(
            Function(obj)
                Thread.Sleep(1000)

                Dim x As Integer = CInt(obj)
                Console.WriteLine("Thread={0}, Next={1}", Thread.CurrentThread.ManagedThreadId, Interlocked.Increment(x))
                Return x
            End Function, number)
    End Function
End Module

' The example displays the similar output:
'     Started RemoteIncrement(0)
'     Started RemoteIncrement(...(RemoteIncrement(RemoteIncrement(4))...)
'     Thread=4, Next=1
'     Finished RemoteIncrement(0)
'     Thread=5, Next=5
'     Thread=6, Next=6
'     Thread=6, Next=7
'     Thread=6, Next=8
'     Finished RemoteIncrement(...(RemoteIncrement(RemoteIncrement(4))...)

Para más información sobre el uso de Unwrap, consulte Cómo: Desencapsular una tarea anidada.

Control de excepciones iniciadas por continuaciones

Una relación antecedente-continuación no es una relación entre elementos primarios y secundarios. Las excepciones que producen las continuaciones no se propagan al antecedente. Por lo tanto, las excepciones producidas por las continuaciones se deben controlar como en cualquier otra tarea, es decir:

  • Puede usar el método Wait, WaitAllo WaitAny , o su homólogo genérico, para esperar en la continuación. Puede esperar un antecedente y sus continuaciones en la misma instrucción try, tal como se muestra en el ejemplo siguiente:
using System;
using System.Threading.Tasks;

public class ExceptionExample
{
    public static async Task Main()
    {
        Task<int> task = Task.Run(
            () =>
            {
                Console.WriteLine($"Executing task {Task.CurrentId}");
                return 54;
            });

        var continuation = task.ContinueWith(
            antecedent =>
            {
                Console.WriteLine($"Executing continuation task {Task.CurrentId}");
                Console.WriteLine($"Value from antecedent: {antecedent.Result}");

                throw new InvalidOperationException();
            });

        try
        {
            await task;
            await continuation;
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}
// The example displays the similar output:
//       Executing task 1
//       Executing continuation task 2
//       Value from antecedent: 54
//       Operation is not valid due to the current state of the object.
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim task1 = Task(Of Integer).Run(Function()
                                             Console.WriteLine("Executing task {0}",
                                                               Task.CurrentId)
                                             Return 54
                                         End Function)
        Dim continuation = task1.ContinueWith(Sub(antecedent)
                                                  Console.WriteLine("Executing continuation task {0}",
                                                                    Task.CurrentId)
                                                  Console.WriteLine("Value from antecedent: {0}",
                                                                    antecedent.Result)
                                                  Throw New InvalidOperationException()
                                              End Sub)

        Try
            task1.Wait()
            continuation.Wait()
        Catch ae As AggregateException
            For Each ex In ae.InnerExceptions
                Console.WriteLine(ex.Message)
            Next
        End Try
    End Sub
End Module
' The example displays the following output:
'       Executing task 1
'       Executing continuation task 2
'       Value from antecedent: 54
'       Operation is not valid due to the current state of the object.
  • Puede usar una segunda continuación para observar la propiedad Exception de la primera continuación. En el siguiente ejemplo, una tarea intenta leer en un archivo inexistente. La continuación muestra entonces información sobre la excepción en la tarea antecedente.
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

public class ExceptionTwoExample
{
    public static async Task Main()
    {
        var task = Task.Run(
            () =>
            {
                string fileText = File.ReadAllText(@"C:\NonexistentFile.txt");
                return fileText;
            });

        Task continuation = task.ContinueWith(
            antecedent =>
            {
                var fileNotFound =
                    antecedent.Exception
                        ?.InnerExceptions
                        ?.FirstOrDefault(e => e is FileNotFoundException) as FileNotFoundException;

                if (fileNotFound != null)
                {
                    Console.WriteLine(fileNotFound.Message);
                }
            }, TaskContinuationOptions.OnlyOnFaulted);

        await continuation;

        Console.ReadLine();
    }
}
// The example displays the following output:
//        Could not find file 'C:\NonexistentFile.txt'.
Imports System.IO
Imports System.Threading.Tasks

Module Example
    Public Sub Main()
        Dim t = Task.Run(Function()
                             Dim s As String = File.ReadAllText("C:\NonexistentFile.txt")
                             Return s
                         End Function)

        Dim c = t.ContinueWith(Sub(antecedent)
                                   ' Get the antecedent's exception information.
                                   For Each ex In antecedent.Exception.InnerExceptions
                                       If TypeOf ex Is FileNotFoundException
                                           Console.WriteLine(ex.Message)
                                       End If
                                   Next
                               End Sub, TaskContinuationOptions.OnlyOnFaulted)

        c.Wait()
    End Sub
End Module
' The example displays the following output:
'       Could not find file 'C:\NonexistentFile.txt'.

Dado que se ejecutó con la opción TaskContinuationOptions.OnlyOnFaulted, la continuación solo se ejecuta si se produce una excepción en el antecedente. Por lo tanto, puede asumir que la propiedad Exception del antecedente no es null. Si la continuación se ejecuta tanto si se produce una excepción en el antecedente como si no, tiene que comprobar si la propiedad Exception del antecedente no es null antes de intentar controlar la excepción, tal como se muestra en el fragmento de código siguiente:

var fileNotFound =
    antecedent.Exception
        ?.InnerExceptions
        ?.FirstOrDefault(e => e is FileNotFoundException) as FileNotFoundException;

if (fileNotFound != null)
{
    Console.WriteLine(fileNotFound.Message);
}
' Determine whether an exception occurred.
If antecedent.Exception IsNot Nothing Then
    ' Get the antecedent's exception information.
    For Each ex In antecedent.Exception.InnerExceptions
        If TypeOf ex Is FileNotFoundException
            Console.WriteLine(ex.Message)
        End If
    Next
End If

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

Vea también