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


Задачи продолжения

В асинхронном программировании очень часто одна асинхронная операция по завершении вызывает вторую операцию и передает в нее данные. Обычно это выполняется с помощью методов обратного вызова. В библиотеке параллельных задач ту же функциональную возможность обеспечивают задачи продолжения. Задача продолжения (также известная как продолжение) — это асинхронная задача, вызываемая другой предшествующей задачей, при ее завершении.

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

  • передать данные из предшествующей задачи в продолжение;

  • указать точные условия, при которых продолжение будет или не будет вызываться;

  • отменить продолжение либо до его запуска, либо совместно во время его выполнения;

  • предоставить подсказки о планировании продолжения;

  • вызвать несколько продолжений из одной и той же предшествующей задачи;

  • вызвать одно продолжение по завершении всех или какой-либо из нескольких предшествующих задач;

  • последовательно соединить продолжения в цепь любой длины;

  • использовать продолжение для обработки исключений, вызванных предшествующей задачей.

Создайте продолжения с помощью метода Task.ContinueWith. В следующем примере показан базовый шаблон (для ясности опущена обработка исключений).

' 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);

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

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();

Продолжение создается в состоянии WaitingForActivation, и поэтому может быть запущено только предшествующей задачей. Для вызова метода Task.Start в продолжении в пользовательском коде создается исключение System.InvalidOperationException.

Продолжение по сути является задачей Task и не блокирует поток, в котором запускается. Чтобы выполнить блокирование до завершения операции продолжения, используйте метод Wait.

Параметры продолжения

При создании продолжения одной задачи можно использовать перегрузку ContinueWith, принимающую перечисление System.Threading.Tasks.TaskContinuationOptions для указания условий, при которых предшествующая задача должна запускать продолжение. Например, можно указать, чтобы продолжение выполнялось только по завершении предшествующей задачи или только по ее завершении в состоянии сбоя и т. д. Если условие не выполняется, когда предшествующая задача готова вызвать продолжение, продолжение переходит непосредственно в состояние Canceled и не может быть запущено после этого. Если указать любой из параметров NotOn или OnlyOn с продолжением нескольких задач, во время выполнения будет создано исключение.

Перечисление System.Threading.Tasks.TaskContinuationOptions также содержит те же параметры, что и перечисление System.Threading.Tasks.TaskCreationOptions. Параметры AttachedToParent, LongRunning и PreferFairness имеют те же значения и содержание в обоих типах перечислений. Эти параметры могут использоваться с продолжениями нескольких задач.

В следующей таблице приводятся все значения объекта TaskContinuationOptions.

Элемент

Описание

None

Задает поведение по умолчанию, если перечисления TaskContinuationOptions не указаны. Продолжение планируется по завершении предшествующей задачи независимо от ее конечного состояния. Если задача является дочерней, она создается как отсоединенная вложенная задача.

PreferFairness

Указывает, что продолжение планируется таким образом, чтобы задачи, запланированные раннее, выполнялись раньше, а более поздние задачи — позже.

LongRunning

Указывает, что продолжение будет длительной подробной операцией. Предоставляет сведения для System.Threading.Tasks.TaskScheduler, что следует ожидать избыточной подписки.

AttachedToParent

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

NotOnRanToCompletion

Указывает, что продолжение не должно планироваться, если его предшествующая задача завершилась.

NotOnFaulted

Указывает, что продолжение не должно планироваться, если его предшествующая задача создала необработанное исключение.

NotOnCanceled

Указывает, что продолжение не должно планироваться, если предшествующая задача была отменена.

OnlyOnRanToCompletion

Указывает, что продолжение должно планироваться, только если его предшествующая задача завершилась.

OnlyOnFaulted

Указывает, что продолжение должно планироваться, только если его предшествующая задача создала необработанное исключение. При использовании параметра OnlyOnFaulted гарантируется, что значение свойства Exception в предшествующей задаче не равно NULL. Это свойство можно использовать для перехвата исключения и определения исключения, вызвавшего сбой задачи. Если не обратиться к свойству Exception, исключение обрабатываться не будет. Кроме того, при попытке доступа к свойству Result задачи, которая была отменена или завершилась ошибкой, будет вызвано новое исключение.

OnlyOnCanceled

Указывает, что продолжение должно планироваться, только если его предшествующая задача завершилась в состоянии Canceled.

ExecuteSynchronously

Для очень коротких продолжений. Указывает, что в идеале продолжение должно выполняться в том же потоке, который приводит к переходу предшествующей задачи в конечное состояние. Если при создании продолжения предшествующая задача уже завершена, система выполнит попытку запустить продолжение в создавшем его потоке. Если объект CancellationTokenSource предшествующей задачи в блоке finally (Finally в Visual Basic) удаляется, продолжение с этим параметром будет запущено в указанном блоке finally.

Передача данных в продолжение

Ссылка на предшествующую задачу передается пользовательскому делегату продолжения как аргумент. Если предшествующая задача является задачей System.Threading.Tasks.Task<TResult> и она завершена, продолжение может получить доступ к свойству Task<TResult>.Result задачи. С продолжением нескольких задач и методом Task.WaitAll аргументом является массив предшествующих задач. При использовании Task.WaitAny аргументом является первая завершенная предшествующая задача.

Свойство Task<TResult>.Result блокируется до завершения задачи. Однако если задача была отменена или завершилась ошибкой, свойство Result создает исключение, когда код пытается получить к нему доступ. Этой проблемы можно избежать, используя параметр OnlyOnRanToCompletion, как показано в следующем примере.

            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);

Если требуется, чтобы продолжение выполнялось даже при незавершенной предшествующей задаче, необходимо защититься от исключения. Одним из способов является проверка состояния предшествующей задачи и только попытка доступа к Result, если состояние отличается от Faulted или Canceled. Также можно проверить свойство Exception предшествующей задачи. Дополнительные сведения см. в разделе Обработка исключений (библиотека параллельных задач).

Отмена продолжения

Продолжение переходит в состояние Canceled в следующих сценариях.

  • Продолжение создает исключение OperationCanceledException в ответ на запрос отмены. Так же как с любой задачей, если исключение содержит тот же токен, что был передан в продолжение, оно рассматривается как подтверждение совместной отмены.

  • Продолжению был передан токен System.Threading.CancellationToken как аргумент, и свойство IsCancellationRequested токена имеет значение true (True) перед запуском продолжения. В таком случае продолжение не запускается и переходит непосредственно в состояние Canceled.

  • Продолжение никогда не запускается, поскольку не выполнено условие, заданное в TaskContinuationOptions. Например, если задача переходит в состояние Faulted, ее продолжение, созданное с помощью параметра NotOnFaulted, перейдет в состояние Canceled и не запустится.

Чтобы предотвратить выполнение продолжения, если его предшествующие задачи отменены, задайте параметр NotOnCanceled при создании продолжения.

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

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();

Если предшествующая задача не была отменена, токен можно по-прежнему использовать для отмены продолжения. Если предшествующая задача была отменена, продолжение не запустится.

После перехода продолжения в состояние Canceled оно может повлиять на последующие продолжения в зависимости от параметров TaskContinuationOptions, указанных для этих продолжений.

Удаленные продолжения не запускаются.

Продолжения и дочерние задачи

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

Обработка исключений, созданных из продолжений

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

  1. Используйте метод Wait, WaitAll или WaitAny либо универсальный эквивалент для ожидания продолжения. Можно ожидать предшествующую задачу и ее продолжение в одном и том же операторе try (Try в Visual Basic), как показано в следующем примере.
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. Используйте второе продолжение, чтобы наблюдать за свойством Exception первого продолжения. Дополнительные сведения см. в разделах Обработка исключений (библиотека параллельных задач) и Практическое руководство. Обработка исключений, создаваемых задачами.

  2. Если продолжение является дочерней задачей и было создано с помощью параметра AttachedToParent, его исключения будут распространяться родительской задачей в вызывающий поток, как и в случае любой другой вложенной дочерней задачи. Дополнительные сведения см. в разделе Вложенные и дочерние задачи.

См. также

Основные понятия

Библиотека параллельных задач

Журнал изменений

Дата

Журнал

Причина

Июнь 2010

Добавлено примечание об асинхронном поведении продолжений.

Обратная связь от клиента.