Выполнять последовательность задач, один за другим

У меня есть последовательность задач, каждая из которых зависит от вывода предыдущей. Я хотел бы представить это как единственный объект Task, результатом которого является вывод конца последовательности. (Если задачи не зависели друг от друга, я мог бы сделать это параллельно, и я использовал бы TaskFactory.ContinueWhenAll.)

Я хотел бы реализовать этот метод:

static Task<TState> AggregateAsync<T, TState>(
    IEnumerable<T> items,
    TState initial,
    Func<TState, T, Task<TState>> makeTask);

Как я могу эффективно выполнять задачи один за другим в последовательности? Я использую С# 4.0, поэтому я не могу использовать async/await, чтобы сделать это для меня.

Изменить: Я мог бы написать AggregateAsync следующим образом:

static Task<TState> AggregateAsync<T, TState>(IEnumerable<T> items, TState initial, Func<TState, T, Task<TState>> makeTask)
{
    var initialTask = Task.Factory.StartNew(() => initial);
    return items.Aggregate(
        initialTask,
        (prevTask, item) =>
            {
                prevTask.Wait(); // synchronous blocking here?
                return makeTask(prevTask.Result, item);
            });
}

Но, конечно же, я получу пакет задач, каждый из которых блокирует синхронное ожидание одного перед ним?

Ответ 1

Простой способ (с помощью Microsoft.Bcl.Async):

static async Task<TState> AggregateAsync<T, TState>(
    this IEnumerable<T> items,
    TState initial,
    Func<TState, T, Task<TState>> makeTask)
{
  var state = initial;
  foreach (var item in items)
    state = await makeTask(state, item);
  return state;
}

Трудный путь:

static Task<TState> AggregateAsync<T, TState>(
    this IEnumerable<T> items,
    TState initial,
    Func<TState, T, Task<TState>> makeTask)
{
  var tcs = new TaskCompletionSource<TState>();
  tcs.SetResult(initial);
  Task<TState> ret = tcs.Task;
  foreach (var item in items)
  {
    var localItem = item;
    ret = ret.ContinueWith(t => makeTask(t.Result, localItem)).Unwrap();
  }
  return ret;
}

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

Ответ 2

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

T item1 = default(T);
T item2 = default(T);
Task<TState> task1 = makeTask(initial, item1);

//create second task
task1.ContinueWith(task => makeTask(task.Result, item2).Result,
                     TaskContinuationOptions.OnlyOnRanToCompletion);

Edit

Извините, я пропустил эту часть

Я хотел бы представить это как один объект Task, результатом которого является вывод конца последовательности.

Для этого вам просто нужно вернуть ссылку на результат последнего вызова ContinueWith.

Task<State> aggregate = task1.ContinueWith(
                                  task => makeTask(task.Result, item2).Result,
                                  TaskContinuationOptions.OnlyOnRanToCompletion);

var res = aggregate .Result; //wait synchronously for the result of the sequence