Tareas de continuación

En la programación asincrónica, es muy común que una operación asincrónica, cuando se completa, invoque una segunda operación y le pase datos. Tradicionalmente, esto se hacía utilizando métodos de devolución de llamada. En la biblioteca TPL (Task Parallel Library, biblioteca de procesamiento paralelo basado en tareas), las tareas de continuación proporcionan la misma funcionalidad. Una tarea de continuación (o, simplemente, una continuación) es una tarea asincrónica invocada por otra tarea, que se denomina antecedente, cuando se completa el antecedente.

Las continuaciones son relativamente fáciles de usar, y no por ello dejan de ser eficaces y flexibles. Por ejemplo, puede:

  • Pasar datos del antecedente a la continuación

  • Especificar las condiciones precisas en las que se invocará o no la continuación

  • Cancelar una continuación antes de iniciarse o, de manera cooperativa, mientras se está ejecutando

  • Proporcionar sugerencias sobre cómo se debería programar la continuación

  • Invocar varias continuaciones desde el mismo antecedente

  • Invocar una continuación cuando se completa una parte o la totalidad de los antecedentes

  • Encadenar las continuaciones una tras otra hasta cualquier longitud arbitraria

  • Usar una continuación para controlar las excepciones producidas por el antecedente

Las continuaciones se crean con el método Task.ContinueWith. En el siguiente ejemplo se muestra el modelo básico (para mayor claridad, se omite el control de excepciones).

' The antecedent task. Can also be created with Task.Factory.StartNew.
Dim taskA As Task(Of DayOfWeek) = New Task(Of DayOfWeek)(Function()
                                                             Return DateTime.Today.DayOfWeek
                                                         End Function)
' The continuation. Its delegate takes the antecedent task
' as an argument and can return a different type.
Dim continuation As Task(Of String) = taskA.ContinueWith(Function(antecedent)
                                                             Return String.Format("Today is {0}", antecedent.Result)
                                                         End Function)
' Start the antecedent.
taskA.Start()

' Use the contuation's result.
Console.WriteLine(continuation.Result)
            // The antecedent task. Can also be created with Task.Factory.StartNew.
            Task<DayOfWeek> taskA = new Task<DayOfWeek>(() => DateTime.Today.DayOfWeek);

            // The continuation. Its delegate takes the antecedent task
            // as an argument and can return a different type.
            Task<string> continuation = taskA.ContinueWith((antecedent) =>
                {
                    return String.Format("Today is {0}.",
                                        antecedent.Result);
                });

            // Start the antecedent.
            taskA.Start();

            // Use the contuation's result.
            Console.WriteLine(continuation.Result);

También puede crear una continuación de varias tareas que se ejecutará cuando una parte o la totalidad de las tareas de una matriz de tareas se haya completado, como se muestra en el siguiente ejemplo.

Dim task1 As Task(Of Integer) = New Task(Of Integer)(Function()
                                                         ' Do some work...
                                                         Return 34
                                                     End Function)

Dim task2 As Task(Of Integer) = New Task(Of Integer)(Function()
                                                         ' Do some work...
                                                         Return 8
                                                     End Function)

Dim tasks() As Task(Of Integer) = {task1, task2}

Dim continuation = Task.Factory.ContinueWhenAll(tasks, Sub(antecedents)
                                                           Dim answer As Integer = tasks(0).Result + tasks(1).Result
                                                           Console.WriteLine("The answer is {0}", answer)


                                                       End Sub)
task1.Start()
task2.Start()
continuation.Wait()
            Task<int>[] tasks = new Task<int>[2];
            tasks[0] = new Task<int>(() =>
            {
                // Do some work... 
                return 34;
            });

            tasks[1] = new Task<int>(() =>
            {
                // Do some work...
                 return 8;
            });

            var continuation = Task.Factory.ContinueWhenAll(
                            tasks,
                            (antecedents) =>
                            {
                                int answer = tasks[0].Result + tasks[1].Result;
                                Console.WriteLine("The answer is {0}", answer);
                            });

            tasks[0].Start();
            tasks[1].Start();
            continuation.Wait();

Una continuación se crea en el estado WaitingForActivation y, por lo tanto, únicamente puede iniciarla su tarea antecedente. Al llamar a Task.Start en una continuación en el código de usuario, se produce una excepción System.InvalidOperationException.

Una continuación es un objeto Task y no bloquea el subproceso en el que se inicia. Use el método Wait para bloquearlo hasta que la tarea de continuación se complete.

Opciones de una continuación

Al crear una continuación de una sola tarea, puede usar una sobrecarga ContinueWith que tome la enumeración System.Threading.Tasks.TaskContinuationOptions para especificar las condiciones en las que la tarea antecedente debe iniciar la continuación. Por ejemplo, puede especificar que la continuación se ejecute solo si el antecedente se ejecutó completamente o solo si se completó con errores, etc. Si no se cumple la condición cuando el antecedente está listo para invocar la continuación, la continuación pasa directamente al estado Canceled y desde ese momento no se podrá iniciar. Si especifica la opción NotOn u OnlyOn con una continuación de varias tareas, se producirá una excepción en tiempo de ejecución.

La enumeración System.Threading.Tasks.TaskContinuationOptions también incluye las mismas opciones que la enumeración System.Threading.Tasks.TaskCreationOptions. AttachedToParent, LongRunning y PreferFairness tienen los mismos significados y valores en ambos tipos de enumeración. Estas opciones se pueden usar con continuaciones de varias tareas.

En la siguiente tabla se muestran todos los valores de TaskContinuationOptions.

Elemento

Descripción

None

Especifica el comportamiento predeterminado cuando no se especifican TaskContinuationOptions. La continuación se programará una vez completado el antecedente, independientemente del estado final de este. Si la tarea es una tarea secundaria, se crea como una tarea anidada desasociada.

PreferFairness

Especifica que la continuación se programará de modo que las tareas programadas antes tengan más posibilidades de ejecutarse antes y las tareas programadas después tengan más posibilidades de ejecutarse más tarde.

LongRunning

Especifica que la continuación será una operación general de larga duración. Proporciona una sugerencia al System.Threading.Tasks.TaskScheduler de que se puede garantizar la sobresuscripción.

AttachedToParent

Especifica que la continuación, si es una tarea secundaria, se adjunta a un elemento primario en la jerarquía de tareas. La continuación es una tarea secundaria solo si su antecedente también es una tarea secundaria.

NotOnRanToCompletion

Especifica que no se debe programar la continuación si su antecedente se ejecuta completamente.

NotOnFaulted

Especifica que no se debe programar la continuación si su antecedente produjo una excepción no controlada.

NotOnCanceled

Especifica que no se debe programar la continuación si se cancela su antecedente.

OnlyOnRanToCompletion

Especifica que la continuación solo se debe programar si el antecedente se ejecuta completamente.

OnlyOnFaulted

Especifica que la continuación solo se debe programar si su antecedente produjo una excepción no controlada. Al usar la opción OnlyOnFaulted, se garantiza que la propiedad Exception del antecedente no es NULL. Puede usar esa propiedad para detectar la excepción y ver qué excepción provocó el error de la tarea. Si no tiene acceso a la propiedad Exception, no se controlará la excepción. Asimismo, si intenta tener acceso a la propiedad Result de una tarea cancelada o con errores, se producirá una nueva excepción.

OnlyOnCanceled

Especifica que la continuación debe programarse únicamente si su antecedente se completa en estado Canceled.

ExecuteSynchronously

Para las continuaciones de muy corta duración. Especifica que lo ideal es que la continuación se ejecute en el mismo subproceso que causa la transición del antecedente a su estado final. Si el antecedente ya se ha completado cuando se crea la continuación, el sistema intentará ejecutar la continuación en el subproceso que la crea. Si se elimina CancellationTokenSource del antecedente en un bloque finally (Finally en Visual Basic), se ejecutará una continuación con esta opción en ese bloque finally.

Pasar datos a una continuación

Una referencia al antecedente se pasa como argumento al delegado de usuario de la continuación. Si el antecedente es System.Threading.Tasks.Task<TResult> y la tarea se ejecutó completamente, la continuación puede tener acceso a la propiedad Task<TResult>.Result de la tarea. Con una continuación de varias tareas y el método Task.WaitAll, el argumento es la matriz de antecedentes. Al usar Task.WaitAny, el argumento es el primer antecedente que se completó.

Task<TResult>.Result se bloquea hasta que la tarea se ha completado. Sin embargo, si la tarea se canceló o tiene errores, Result produce una excepción cuando el código intenta tener acceso al mismo. Puede evitar este problema mediante la opción OnlyOnRanToCompletion, como se muestra en el siguiente ejemplo.

            Dim aTask = Task(Of Integer).Factory.StartNew(Function()
                                                              Return 54
                                                          End Function)
            Dim bTask = aTask.ContinueWith(Sub(antecedent)
                                               Console.WriteLine("continuation {0}", antecedent.Result)
                                           End Sub,
                                           TaskContinuationOptions.OnlyOnRanToCompletion)

var t = Task<int>.Factory.StartNew(() => 54);

var c = t.ContinueWith((antecedent) =>
{
    Console.WriteLine("continuation {0}", antecedent.Result);
},
    TaskContinuationOptions.OnlyOnRanToCompletion);

Si desea que la continuación se ejecute aunque el antecedente no se ejecute completamente, debe usar medidas de protección contra la excepción. Un posible enfoque es probar el estado del antecedente e intentar tener acceso a Result solamente si el estado no es Faulted o Canceled. También puede examinar la propiedad Exception del antecedente. Para obtener más información, vea Control de excepciones (Task Parallel Library).

Cancelar una continuación

Una continuación pasa al estado Canceled en estos escenarios:

  • Cuando produce una excepción OperationCanceledException en respuesta a una solicitud de cancelación. Al igual que sucede con cualquier tarea, si la excepción contiene el mismo token que se pasó a la continuación, se trata como una confirmación de cancelación cooperativa.

  • Cuando se pasa System.Threading.CancellationToken como argumento a la continuación y la propiedad IsCancellationRequested del token es true (True) antes de ejecutar la continuación. En este caso, la continuación no se inicia y pasa directamente al estado Canceled.

  • Cuando la continuación nunca se ejecuta porque no se cumple la condición establecida en TaskContinuationOptions. Por ejemplo, si una tarea entra en estado Faulted, su continuación, creada con la opción NotOnFaulted, pasará al estado Canceled y no se ejecutará.

Para que una continuación no se ejecute si su antecedente se cancela, especifique la opción NotOnCanceled al crear la continuación.

Si una tarea y su continuación representan dos partes de la misma operación lógica, puede pasar el mismo token de cancelación a ambas tareas, como se muestra en el siguiente ejemplo.

Dim someCondition As Boolean = True
Dim cts As New CancellationTokenSource
Dim task1 = New Task(Sub()
                         Dim ct As CancellationToken = cts.Token
                         While someCondition = True
                             ct.ThrowIfCancellationRequested()
                             ' Do the work here...
                             ' ...
                         End While
                     End Sub,
                     cts.Token
                     )

Dim task2 = task1.ContinueWith(Sub(antecedent)
                                   Dim ct As CancellationToken = cts.Token
                                   While someCondition = True
                                       ct.ThrowIfCancellationRequested()
                                       ' Do the work here
                                       ' ...
                                   End While
                               End Sub,
                               cts.Token)
task1.Start()
' ...
' Antecedent and/or continuation will
' respond to this request, depending on when it is made.
cts.Cancel()
Task task = new Task(() =>
{
    CancellationToken ct = cts.Token;
    while (someCondition)
    {
        ct.ThrowIfCancellationRequested();
        // Do the work.
        //...                        
    }
},
    cts.Token
    );

Task task2 = task.ContinueWith((antecedent) =>
{
    CancellationToken ct = cts.Token;

    while (someCondition)
    {
        ct.ThrowIfCancellationRequested();
        // Do the work.
        //...                        
    }
},
    cts.Token);

task.Start();
//...

// Antecedent and/or continuation will 
// respond to this request, depending on when it is made.
cts.Cancel();

Si el antecedente no se cancela, todavía se puede usar el token para cancelar la continuación. Si el antecedente se cancela, no se inicia la continuación.

Después de que una continuación entra en estado Canceled, puede afectar a las continuaciones posteriores, dependiendo de las opciones TaskContinuationOptions especificadas para esas continuaciones.

Las continuaciones eliminadas no se inician.

Continuaciones y tareas secundarias

Una continuación no se ejecuta hasta que se completan su antecedente y todas las tareas secundarias asociadas. La continuación no espera a que se completen las tareas secundarias desasociadas. El estado final de la tarea antecedente depende del estado final de cualquier tarea secundaria asociada. El estado de las tareas secundarias desasociadas no afecta a la tarea primaria. Para obtener más información, vea Tareas anidadas y tareas secundarias.

Controlar las excepciones que producen las continuaciones

Una relación entre un antecedente y una continuación no es una relación primario-secundario. Las excepciones producidas por las continuaciones no se propagan al antecedente. Por consiguiente, las excepciones que producen las continuaciones se deben controlar de igual modo que en cualquier otra tarea, como se indica a continuación.

  1. Use el método Wait, WaitAny o WaitAll, o su homólogo genérico, para esperar en la continuación. Puede esperar a un antecedente y sus continuaciones en la misma instrucción try (Try en Visual Basic), como se muestra en el siguiente ejemplo.
Dim task1 = Task(Of Integer).Factory.StartNew(Function()
                                                  Return 54
                                              End Function)
Dim continuation = task1.ContinueWith(Sub(antecedent)
                                          Console.WriteLine("continuation {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

Console.WriteLine("Exception handled. Let's move on.")
var t = Task<int>.Factory.StartNew(() => 54);

var c = t.ContinueWith((antecedent) =>
{
    Console.WriteLine("continuation {0}", antecedent.Result);
    throw new InvalidOperationException();
});

try
{
    t.Wait();
    c.Wait();
}

catch (AggregateException ae)
{
    foreach(var e in ae.InnerExceptions)
        Console.WriteLine(e.Message);
}
Console.WriteLine("Exception handled. Let's move on.");
  1. Use una segunda continuación para observar la propiedad Exception de la primera continuación. Para obtener más información, vea Control de excepciones (Task Parallel Library) y Cómo: Controlar excepciones iniciadas por tareas.

  2. Si la continuación es una tarea secundaria creada mediante la opción AttachedToParent, la tarea primaria propagará sus excepciones al subproceso que realiza la llamada, como sucede con cualquier otro elemento secundario asociado. Para obtener más información, vea Tareas anidadas y tareas secundarias.

Vea también

Conceptos

Task Parallel Library

Historial de cambios

Fecha

Historial

Motivo

Junio de 2010

Nota agregada sobre el comportamiento asincrónico de las continuaciones.

Comentarios de los clientes.