【问题标题】:Run sequence of tasks, one after the other一个接一个地运行一系列任务
【发布时间】:2013-12-06 10:50:14
【问题描述】:

我有一系列任务,其中每个任务都取决于前一个任务的输出。我想将其表示为单个 Task 对象,其结果是序列末尾的输出。 (如果任务不相互依赖,那么我可以并行执行,我会使用TaskFactory.ContinueWhenAll。)

我希望能够实现这个方法:

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

如何高效地依次执行任务?我使用的是 C# 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);
            });
}

但我肯定会得到一批任务,每个任务块同步等待它之前的一个?

【问题讨论】:

标签: c# .net c#-4.0 asynchronous


【解决方案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 中。 “简单”的方式不会像这样包装异常。

【讨论】:

  • 谢谢斯蒂芬 - 看起来TaskExtensions.Unwrap 是神奇的方法。还有example on MSDN。和我的情况类似。
  • 感谢.Unwrap“技巧”。另外,不能用Task.FromResult(initial)替换你的“硬方式”的前三行吗?有什么区别吗?
  • @dcastro:是的,但 FromResult 在 .NET 4.0 上不可用。但从逻辑上讲,这就是它正在做的事情。
  • 我对@9​​87654330@ 的要求感到困惑。似乎 MS 的设计很愚蠢。
  • @Gusdor:这对我来说很有意义;如果你有一个任务返回一个状态任务,正确的类型 Task&lt;Task&lt;TState&gt;&gt;,你可以将Unwrap转换成普通的Task&lt;TState&gt;。在某些情况下,Task&lt;Task&lt;TState&gt;&gt; 正是您想要的。有点尴尬的原因是因为我们在 .NET 4.0 中进行异步编程(当时 Task 类型主要用于并行编程)。
【解决方案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);

编辑

对不起,我错过了这部分

我想将其表示为单个 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

【讨论】:

  • 在 F# 和 JS 中完成此操作后,我想要一个可以创建新任务的 ContinueWith - 即 Func&lt;Task&lt;T&gt;, Task&lt;TResult&gt;&gt;。但是只有Func&lt;Task&lt;T&gt;, TResult&gt;
  • 什么意思?我可能误解了你。我很肯定你可以使用我提供的示例轻松实现具有给定签名的方法,以及你自己的makeTask 实现。
  • 只需使用.ContinueWith 链接您的所有任务,结果将是您可以等待的单个任务实例(例如使用 dcastro 显示的 Task.Result)。任务将按顺序完成。那就是 - task1.ContinueWith(...).ContinueWith(...).ContinueWith(...) 或类似的东西。
  • 您的第二个示例如何应用于任意序列?如果我return secondTask;,我会得到一个Task&lt;Task&lt;T&gt;&gt;;如果我return thirdTask; 我得到Task&lt;Task&lt;Task&lt;T&gt;&gt;&gt;,等等。我用我认为可以实现我想要的代码但使用阻塞更新了问题。
  • @TimRobinson 我的错,我已经修复了我的示例以返回 Task&lt;State&gt; 而不是 Task&lt;Task&lt;State&gt;&gt;。在这段代码中,没有阻塞。当您在上一个任务上调用.Result 时,该任务已经运行完成。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-05-14
  • 2018-03-30
  • 2014-06-21
  • 1970-01-01
  • 2014-07-15
  • 1970-01-01
相关资源
最近更新 更多