【问题标题】:Good pattern for exception handling when using async calls使用异步调用时异常处理的良好模式
【发布时间】:2014-03-08 20:51:57
【问题描述】:

我想使用一个 Web API,我看到很多人推荐 System.Net.Http.HttpClient

没关系...但我只有 VS-2010,所以我还不能使用async/await。相反,我想我可以将Task<TResult>ContinueWith 结合使用。所以我尝试了这段代码:

var client = new HttpClient();
client.DefaultRequestHeaders.Accept.Add(
    new MediaTypeWithQualityHeaderValue("application/json"));

client.GetStringAsync(STR_URL_SERVER_API_USERS).ContinueWith(task =>
{                 
   var usersResultString = task.Result;
   lbUsers.DataSource = JsonConvert.DeserializeObject<List<string>>(usersResultString);
});

我的第一个观察是意识到如果 URL 不可用它不会产生任何错误,但可能会有更多这样的错误......

所以我试图找到一种方法来处理此类异步调用的异常(特别是对于 HttpClient)。我注意到“任务”具有IsFaulted 属性和AggregateException 可能可以使用,但我不确定如何使用。

另一个观察结果是GetStringAsync 返回Task&lt;string&gt;,但GetAsync 返回Task&lt;HttpResponseMessage&gt;。后者可能更有用,因为它提供了StatusCode

您能否分享一个关于如何正确使用异步调用并以良好方式处理异常的模式?基本解释也将不胜感激。

【问题讨论】:

    标签: c# .net-4.0 exception-handling task-parallel-library dotnet-httpclient


    【解决方案1】:

    对于成功和错误的场景,我不会使用单独的 ContinueWith 延续。我宁愿在一个地方处理这两种情况,使用try/catch

    task.ContinueWith(t =>
       {
          try
          {
              // this would re-throw an exception from task, if any
              var result = t.Result;
              // process result
              lbUsers.DataSource = JsonConvert.DeserializeObject<List<string>>(result);
          }
          catch (Exception ex)
          {
              MessageBox.Show(ex.Message);
              lbUsers.Clear();
              lbUsers.Items.Add("Error loading users!");
          }
       }, 
       CancellationToken.None, 
       TaskContinuationOptions.None, 
       TaskScheduler.FromCurrentSynchronizationContext()
    );
    

    如果t 是非泛型Task(而不是Task&lt;TResult&gt;),您可以使用t.GetAwaiter().GetResult()ContinueWith lambda 中重新抛出原始异常; t.Wait() 也可以。准备好处理AggregatedException,您可以通过以下方式处理内部异常:

    catch (Exception ex)
    {
        while (ex is AggregatedException && ex.InnerException != null)
            ex = ex.InnerException;
    
        MessageBox.Show(ex.Message);
    }
    

    如果您正在处理一系列ContinueWith,通常您不必在每个 ContinueWith 中处理异常。对最外层的结果任务执行一次,例如:

    void GetThreePagesV1()
    {
        var httpClient = new HttpClient();
        var finalTask = httpClient.GetStringAsync("http://example.com")
            .ContinueWith((task1) =>
                {
                    var page1 = task1.Result;
                    return httpClient.GetStringAsync("http://example.net")
                        .ContinueWith((task2) =>
                            {
                                var page2 = task2.Result;
                                return httpClient.GetStringAsync("http://example.org")
                                    .ContinueWith((task3) =>
                                        {
                                            var page3 = task3.Result;
                                            return page1 + page2 + page3;
                                        }, TaskContinuationOptions.ExecuteSynchronously);
                            }, TaskContinuationOptions.ExecuteSynchronously).Unwrap();
                }, TaskContinuationOptions.ExecuteSynchronously).Unwrap()
            .ContinueWith((resultTask) =>
                {
                    httpClient.Dispose();
                    string result = resultTask.Result;
                    try
                    {
                        MessageBox.Show(result);
                    }
                    catch (Exception ex)
                    {
                        MessageBox.Show(ex.Message);
                    }
                },
                CancellationToken.None,
                TaskContinuationOptions.None,
                TaskScheduler.FromCurrentSynchronizationContext());
    }
    

    当您访问内部任务的结果 (taskN.Result) 时,内部任务中引发的任何异常都会传播到最外层的 ContinueWith lambda。

    这段代码可以正常工作,但也很丑陋且不可读。 JavaScript 开发人员将其称为末日回调金字塔。他们有Promises 来处理它。 C# 开发人员有 async/await,很遗憾,由于 VS2010 的限制,您无法使用它。

    IMO,与 TPL 中的 JavaScript Promises 最接近的是 Stephen Toub's Then pattern。在 C# 4.0 中与 async/await 最接近的是他在同一篇博文中的 Iterate 模式,它使用了 C# yield 功能。

    使用Iterate 模式,可以以更易读的方式重写上述代码。请注意,在 GetThreePagesHelper 中,您可以使用所有熟悉的同步代码语句,例如 usingforwhiletry/catch 等。但是了解此模式的异步代码流非常重要:

    void GetThreePagesV2()
    {
        Iterate(GetThreePagesHelper()).ContinueWith((iteratorTask) =>
            {
                try
                {
                    var lastTask = (Task<string>)iteratorTask.Result;
    
                    var result = lastTask.Result;
                    MessageBox.Show(result);
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message);
                    throw;
                }
            },
            CancellationToken.None,
            TaskContinuationOptions.None,
            TaskScheduler.FromCurrentSynchronizationContext());
    }
    
    IEnumerable<Task> GetThreePagesHelper()
    {
        // now you can use "foreach", "using" etc
        using (var httpClient = new HttpClient())
        {
            var task1 = httpClient.GetStringAsync("http://example.com");
            yield return task1;
            var page1 = task1.Result;
    
            var task2 = httpClient.GetStringAsync("http://example.net");
            yield return task2;
            var page2 = task2.Result;
    
            var task3 = httpClient.GetStringAsync("http://example.org");
            yield return task3;
            var page3 = task3.Result;
    
            yield return Task.Delay(1000);
    
            var resultTcs = new TaskCompletionSource<string>();
            resultTcs.SetResult(page1 + page1 + page3);
            yield return resultTcs.Task;
        }
    }
    
    /// <summary>
    /// A slightly modified version of Iterate from 
    /// http://blogs.msdn.com/b/pfxteam/archive/2010/11/21/10094564.aspx
    /// </summary>
    public static Task<Task> Iterate(IEnumerable<Task> asyncIterator)
    {
        if (asyncIterator == null)
            throw new ArgumentNullException("asyncIterator");
    
        var enumerator = asyncIterator.GetEnumerator();
        if (enumerator == null)
            throw new InvalidOperationException("asyncIterator.GetEnumerator");
    
        var tcs = new TaskCompletionSource<Task>();
    
        Action<Task> nextStep = null;
        nextStep = (previousTask) =>
        {
            if (previousTask != null && previousTask.Exception != null)
                tcs.SetException(previousTask.Exception);
    
            if (enumerator.MoveNext())
            {
                enumerator.Current.ContinueWith(nextStep,
                    TaskContinuationOptions.ExecuteSynchronously);
            }
            else
            {
                tcs.SetResult(previousTask);
            }
        };
    
        nextStep(null);
        return tcs.Task;
    }
    

    【讨论】:

    • 再次感谢这个伟大而完整的答案。据我所知,您的代码中有很多我不明白的细节......可怜的我,我只是一个初学者。我必须承认我发现很难掌握所有这些 TPL 的东西......尤其是在 VS2010 世界中。很奇怪,我没有早点放弃它,而是为我的世界选择更简单的东西,比如 HttpWebRequest :) ...哦,男孩,做出艰难的决定
    • @Cristi,使用HttpWebRequest 不会有很大的不同。即,你可以只做var text = httpClient.GetStringAsync("http://example.net").Result,这将是一个阻塞同步调用(因为Result)。随时澄清此处或单独问题中不清楚的任何内容。另外,请尝试在调试器中单步执行此代码,这可能有助于理解它。
    • 你绝对是真的!一个小问题:为什么我的代码在 URL 不可用的情况下不会抛出任何异常?
    • 好的,我找到了上一个问题的答案:stackoverflow.com/questions/17151215/…
    • @Cristi,.NET 4.0 中的异常处理与 v4.5 略有不同,请查看Tasks and Unhandled Exceptions
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-11
    • 1970-01-01
    • 1970-01-01
    • 2012-12-27
    • 2014-09-28
    相关资源
    最近更新 更多