在处理WhenAll时优雅地处理异常

时间:2015-03-11 09:55:23

标签: c# async-await

我正在玩一段我写过的代码。这段代码处理以异步方式发出一些请求。

var client = new HttpClient();

var searchPromises = searchTerms
    .Select(GetSearchUrl)
    .Select(client.GetStringAsync);
var searchPages = await Task.WhenAll(searchPromises);

我会创建一个新的HttpClient。使用一些搜索引擎术语我组成搜索引擎网址。然后我使用这些URL作为输入来获取表示具有结果的页面的异步请求的任务。最后,我等待那些使用Task.WhenAll将它们组合在一起的回复。

问题是,如果其中一个请求获得404,500或类似我的代码抛出AggregateException

是否有一种方法可以指定在其中一个线程中出现错误时应该发生什么,以便从其他所有内容中获得结果?

我看过ContinueWith,但它似乎不适合这个账单,也就是说,它不知道如何处理所有错误,只是汇总错误。

4 个答案:

答案 0 :(得分:6)

  

我会创建一个新的HttpClient。使用一些搜索引擎术语我组成搜索引擎网址。然后我使用这些URL作为输入来获取表示具有结果的页面的异步请求的任务。最后,我等待那些使用Task.WhenAll将它们组合在一起的响应。

     

是否有一种方法可以指定在其中一个线程中出现错误时应该发生什么,以便从其他所有内容中获得结果?

IMO,最简单的解决方案是改变您对问题的看法。现在,您正在思考"在每个网址上进行下载"然后"为了他们所有人完成和处理每个项目的错误"。只需更改您的操作("下载")即可包含您想要按项目执行的任何操作。换句话说,你想要做的是"在每个网址上执行下载并处理错误"然后"等待他们全部完成":

var client = new HttpClient();
var searchPromises = searchTerms
  .Select(GetSearchUrl)
  .Select(url => DownloadAsync(client, url));
var searchPages = await Task.WhenAll(searchPromises);
var successfulSearchPages = searchPages.Where(x => x != null);

...

private static async Task<string> DownloadAsync(HttpClient client, string url)
{
  try
  {
    return await client.GetStringAsync(url);
  }
  catch (HttpRequestException ex)
  {
    // TODO: Perform appropriate error handling
    return null;
  }
}

答案 1 :(得分:3)

Task.WhenAll将返回在完成作为参数传递的所有任务时完成的任务。

如果作为参数传递的任何任务以Faulted状态结束(抛出异常),则返回的任务也将以Faulted状态结束,其Exception属性将包含作为参数传递的任务抛出的所有异常的聚合。

因为编译器生成的代码选择了列表中的第一个exceptin,所以只会重新抛出抛出的第一个异常(不是第一个异常抛出)抛出的excpetion。

但是作为参数传递的任务仍然存在,仍然可以查询结果。

此代码段显示了这项工作:

var tasks = new Task[] {
    ((Func<Task>)(async () =>
    {
        await Task.Delay(10);
        await Task.Delay(10);
        await Task.Delay(10);
        throw new Exception("First");
    }))(),
    ((Func<Task>)(async () =>
    {
        await Task.Delay(10);
        throw new Exception("Second");
    }))(),
    ((Func<Task>)(async () =>
    {
        await Task.Delay(10);
    }))()
};
var allTasks = Task.WhenAll(tasks);
try
{
    await allTasks;
}
catch (Exception ex)
{
    Console.WriteLine("Overall failed: {0}", ex.Message);
}


for(var i = 0; i < tasks.Length; i++)
{
    try
    {
        await tasks[i];
        Console.WriteLine("Taks {0} succeeded!", i);
    }
    catch (Exception ex)
    {
        Console.WriteLine("Taks {0} failed!", i);
    }
}

/*
Overall failed: First
Taks 0 failed!
Taks 1 failed!
Taks 2 succeeded!
*/

答案 2 :(得分:2)

您可以使用Task.WhenAll创建自己的Task.WhenAny版本,仅返回结果而忽略任何异常:

public static async Task<IEnumerable<TResult>> WhenAllSwallowExceptions<TResult>(IEnumerable<Task<TResult>> tasks)
{
    var tasklist = tasks.ToList();
    var results = new List<TResult>();
    while (tasklist.Any())
    {
        var completedTask = await Task.WhenAny(tasklist);
        try
        {
            results.Add(await completedTask);
        }
        catch (Exception e)
        {
            // handle
        }

        tasklist.Remove(completedTask);
    }

    return results;
}

用法:

var searchPages = await WhenAllSwallowExceptions(searchPromises);

这一次等待一个任务(使用Task.WhenAny)并聚合所有结果(如果有的话)。

答案 3 :(得分:0)

经过多次迭代后,我找到了一种方法。任务开始看起来像你需要一个库来抽象的东西。

无论如何,这里的代码是:

var client = new HttpClient();
var exceptions = new ConcurrentBag<Exception>();

var searchPromises = searchTerms
    .Select(GetSearchUrl)
    .Select(client.GetStringAsync)
    .Select(t=>t.Catch(e=>exceptions.Add(e)));

var searchPages = (await Task.WhenAll(searchPromises))
    .Where(r => r != null);

Catch的实施:

    public static Task<TResult> Catch<TResult>(this Task<TResult> self, Action<Exception> exceptionHandlerTask)
    {

        return self.ContinueWith(s =>
        {
            if (!s.IsFaulted)
            {
                return s.Result;
            }
            exceptionHandlerTask(s.Exception);
            return default(TResult);
        },
        CancellationToken.None,
        TaskContinuationOptions.ExecuteSynchronously |
        TaskContinuationOptions.DenyChildAttach,
        TaskScheduler.Default);
    }

现在发生的是它为您提供了一种方法,可以将失败状态函数附加到Task<T>承诺。这使我仍然具有可链接性。令人遗憾的是,c#对功能模式匹配没有强大的支持,使这更容易。

编辑:为错误记录添加了最少的代码。

编辑:将用于记录错误的代码分离为更通用/可重用。

编辑:将用于保存错误的代码与Catch功能分开。