Async/await、TaskEx.WhenAll和异常

3

这只是一个想象中的问题,我希望解决方案能够适用于各种类似的场景。假设我需要计算网页上所有外部资源(图片、脚本等)的总大小。我下载页面,提取所有SRC信息,并将URL列表转换为下载任务:

async Task<int> GetTotalSize(Uri uri) {
  string[] urls = ... code to extract all external resources' URLs from given page ...

  var tasks = from url in urls.Distinct()
                select new WebClient().DownloadDataTaskAsync(new Uri(url));
  var files = await TaskEx.WhenAll(tasks);
  return files.Sum(file => file.Length);
}

现在,如果由于任何原因其中一个链接无法访问,则整个TaskEx.WhenAll将被WebException中止。我需要忽略单个任务中的任何WebExceptions,并在这种情况下假定长度为0。有什么想法吗?


好的,它是WebClient.DownloadDataTaskAsync()... - Endrju
糟糕,脑抽了,对不起。 - Frédéric Hamidi
我知道这是一个非常老的问题,我怀疑你仍然在面对同样的问题。但我会创建一个类来封装处理网页所需的所有信息:页面数据、异常、成功/失败状态等。然后在一个方法中调用WebClient().DownloadDataTaskAsync(),该方法返回此类的实例。 - HiredMind
@HiredMind 非常感谢您的评论。我喜欢Jeff的答案,主要是因为它不需要单独的类。我是函数式编程的(正在成为)粉丝,因此我必须(也想)讨厌有状态的对象,特别是如果它们不需要。我更喜欢在Select()、Where()和Aggregate()中完成大部分工作,并将类留给UI/Web,否则会很困难。 - Endrju
实际上,我指的是非基元返回值 - 而不是返回Task<int>,返回Task<DownloadResult>。 Task<>类是唯一保留任何状态的东西。我也讨厌有状态的对象 :-) - HiredMind
3个回答

5
只需添加一个单独的(异步)方法来获取单个url的大小,然后将它们相加。
例如:
static async Task<int> GetTotalSizeAsync(params string[] urls)
{
    if (urls == null)
        return 0;
    var tasks = urls.Select(GetSizeAsync);
    var sizes = await TaskEx.WhenAll(tasks);
    return sizes.Sum();
}

static async Task<int> GetSizeAsync(string url)
{
    try
    {
        var str = await new WebClient().DownloadStringTaskAsync(url);
        return str.Length;
    }
    catch (WebException)
    {
        return 0;
    }
}

谢谢,非常简洁优雅。工作得很好,而且最重要的是它仍然是异步的。我一直在考虑如何将一些东西放入.Select()中。 - Endrju
旧帖子,但我想指出OP所请求的将会并行执行所有调用。解决方案是异步的,但不再并行执行。 - Dan Friedman
@Dan:它实际上是并行完成的。循环中每个迭代都以异步方式运行有效地使其并行化。 - Jeff Mercado
当代码执行到 await new WebClient().DownloadStringTaskAsync(url) 时,线程将停止工作直到它完成。为了使其并行化,您需要删除 await 关键字。 - Dan Friedman
@DanFriedman ... 别说我疯了,但 WhenAll 不是会启动许多任务(每个 URL 一个)吗?因此实际上,如果单个 URL 获取线程在完成之前停止工作,它应该更有效率。这样对吗? - Dan Esparza
WhenAll适用于异步和并行执行。每个URL都会启动一个任务,内部等待每个任务完成,然后在所有任务完成后恢复执行。如果每个URL请求需要1秒钟,串行执行将需要N秒钟,而并行执行只需要1秒钟。 - Dan Friedman

3

这个解决方案允许异步和并行执行,而Jeff目前接受的答案则不支持。

var tasks = from url in urls.Distinct()
            select new WebClient().DownloadDataTaskAsync(new Uri(url));

try
{
    await TaskEx.WhenAll(tasks);
}
catch(Exception)
{
}

var files = tasks
    .Where(f => !f.IsFaulted)
    .Select(f => f.Result);

return files.Sum(file => file.Length);

来源:https://dev59.com/0HDYa4cB1Zd3GeqPCZ5h#15857555


我喜欢这个解决方案。 - tofutim

0

请不要在发帖中添加签名,您的个人资料页面和用户卡已经可以展示您的信息了。 - Jeff Mercado
问题不在于它抛出了AggregateException(我可以通过catch(WebException)轻松地捕获它),而是单个任务中的单个异常会中止所有操作。无论如何,感谢您的建议和文章。 - Endrju

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接