【问题标题】:Task.WhenAll on List<Task> behaving differently than Task.WhenAll on IEnumerable<Task>List<Task> 上的 Task.WhenAll 的行为不同于 IEnumerable<Task> 上的 Task.WhenAll
【发布时间】:2020-04-06 19:20:09
【问题描述】:

在尝试捕获异常时调用 Task.WhenAll(IEnumerable&lt;Task&lt;T&gt;&gt;) 和调用 Task.WhenAll(List&lt;Task&lt;T&gt;&gt;) 时,我看到了一些奇怪的行为差异

我的代码如下:

public async Task Run()
{
    var en = GetResources(new []{"a","b","c","d"});
    await foreach (var item in en)
    {
        var res = item.Select(x => x.Id).ToArray();
        System.Console.WriteLine(string.Join("-> ", res));
    }
}

private async IAsyncEnumerable<IEnumerable<ResponseObj>> GetResources(
    IEnumerable<string> identifiers)
{
    IEnumerable<IEnumerable<string>> groupedIds = identifiers.Batch(2);
        // MoreLinq extension method -- batches IEnumerable<T>
        // into IEnumerable<IEnumerable<T>>
    foreach (var batch in groupedIds)
    {
        //GetHttpResource is simply a wrapper around HttpClient which
        //makes an Http request to an API endpoint with the given parameter
        var tasks = batch.Select(id => ac.GetHttpResourceAsync(id)).ToList();
            // if I remove this ToList(), the behavior changes
        var stats = tasks.Select(t => t.Status);
            // at this point the status being WaitingForActivation is reasonable
            // since I have not awaited yet
        IEnumerable<ResponseObj> res = null;
        var taskGroup = Task.WhenAll(tasks);
        try
        {
            res = await taskGroup;
            var awaitedStats = tasks.Select(t => t.Status);
                //this is the part that changes
                //if I have .ToList(), the statuses are RanToCompletion or Faulted
                //if I don't have .ToList(), the statuses are always WaitingForActivation
        }
        catch (Exception ex)
        {
            var exceptions = taskGroup.Exception.InnerException;
            DoSomethingWithExceptions(exceptions);
            res = tasks.Where(g => !g.IsFaulted).Select(t => t.Result);
                //throws an exception because all tasks are WaitingForActivation
        }
        yield return res;
    }
}

最终,我有一个 IEnumerable 的标识符,我将其分成 2 个批次(在此示例中为硬编码),然后运行 ​​Task.WhenAll 以同时运行每批 2 个。

我想要的是,如果 2 个 GetResource 任务中的一个失败,仍然返回另一个的成功结果,并处理异常(例如,将其写入日志)。

如果我在任务列表上运行Task.WhenAll,这完全符合我的要求。但是,如果我删除.ToList(),当我尝试在await taskGroup 之后的catch 块中找到我的错误任务时,我会遇到问题,因为我的任务状态仍然是WaitingForActivation,尽管我相信它们已经被等待.

当没有抛出异常时,ListIEnumerable 的行为方式相同。这只会在我尝试捕获异常时开始引起问题。

这种行为背后的原因是什么? Task.WhenAll 必须在我进入 catch 块后完成,但是为什么状态仍然是 WaitingForActivation?我没有掌握这里的基本知识吗?

【问题讨论】:

  • ToList 正在更早地开始任务......但这不重要。您能否查看minimal reproducible example 指南和edit 帖子以澄清yield return / IAsyncEnumerable 是否与问题有关?
  • “此时的状态是 WaitingForActivation,这是我所期望的” - 通常对代码中所示的已启动任务的期望是错误的......显示简化的 GetResourceAsync 会很好。跨度>
  • 除非您使列表具体化(使用ToList()),否则每次枚举列表时,您都会再次调用GetHttpResourceAsync,并创建一个新任务。这是由于惰性评估。在处理任务列表时,我肯定会保留ToList() 电话。
  • John Wu 上面的评论完全解释了你的观察。附带说明一下,如果您为了控制并发级别而对输入进行批处理,那么TPL Dataflow 库是一个更好的工具。可能您只需要一个TransformBlockMaxDegreeOfParallelism = 2Here 就是一个例子。
  • 要从TransformBlock 获取所有结果并稍后将它们保存到数据库,您可能不需要将结果作为IAsyncEnumerable 流回。 Task&lt;List&lt;ResponseObj&gt;&gt; 的返回值似乎更合适。 Here 是一个 ToListAsync 扩展方法,用于数据流块,正是这样做的。获得结果后,您可以将它们分批保存,将BatchBlock&lt;ResponseObj&gt; 链接到ActionBlock&lt;ResponseObj[]&gt;

标签: c# asynchronous async-await task iasyncenumerable


【解决方案1】:

除非您使列表具体化(通过使用ToList()),否则每次枚举列表时,您都会再次调用GetHttpResourceAsync,并创建一个新任务。这是由于deferred execution

在处理任务列表时,我肯定会保留ToList() 电话

【讨论】:

    猜你喜欢
    • 2017-03-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-12-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多