使用异步调用时异常处理的良好模式 [英] Good pattern for exception handling when using async calls

查看:29
本文介绍了使用异步调用时异常处理的良好模式的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I want to consume an Web API and I see many people recommending System.Net.Http.HttpClient.

那很好...但我只有 VS-2010,所以我现在还不能使用 async/await.相反,我想我可以将 TaskContinueWith 结合使用.所以我尝试了这段代码:

That's fine... but I have only VS-2010, so I cannot use async/await just yet. Instead, I guess I could use Task<TResult> in combination to ContinueWith. So I tried this piece of code:

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 不可用它不会产生任何错误,但也许会有更多这样的错误......

My first observation was to realize that it doesn't generate any error if URL is not available, but maybe there will be more errors like this...

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

So I am trying to find a way to handle exceptions for such async calls (particularly for HttpClient). I noticed that "Task" has IsFaulted property and an AggregateException which maybe could be used, but I am not sure yet how.

另一个观察结果是 GetStringAsync 返回 Task,但 GetAsync 返回 Task.后者可能更有用,因为它提供了一个 StatusCode.

Another observation was that GetStringAsync returns Task<string>, but GetAsync returns Task<HttpResponseMessage>. The latter could be maybe more useful, since it presents a StatusCode.

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

Could you share a pattern on how to use the async calls correctly and handle exceptions in a good way? Basic explanation would be appreciated as well.

推荐答案

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

I would not use a separate ContinueWith continuation for successful and faulted scenarios. I'd rather handle both cases in a single place, using 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),你可以做 t.GetAwaiter().GetResult()ContinueWith lambda 中重新抛出原始异常;t.Wait() 也可以.准备好处理AggregatedException,你可以用这样的东西进入内部异常:

If t is a non-generic Task (rather than a Task<TResult>), you can do t.GetAwaiter().GetResult() to re-throw the original exception inside the ContinueWith lambda; t.Wait() would work too. Be prepared to handle AggregatedException, you can get to the inner exception with something like this:

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

    MessageBox.Show(ex.Message);
}

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

If you're dealing with a series of ContinueWith, usually you don't have to handle exceptions inside each ContinueWith. Do it once for the outermost resulting task, e.g.:

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.

Any exceptions thrown inside inner tasks will propagate to the outermost ContinueWith lambda as you're accessing the results of the inner tasks (taskN.Result).

这段代码很实用,但也很丑陋且不可读.JavaScript 开发人员称之为末日回调金字塔.他们有承诺来处理它.C# 开发人员有 async/await,很遗憾,由于 VS2010 限制,您无法使用.

This code is functional, but it's also ugly and non-readable. JavaScript developers call it The Callback Pyramid of Doom. They have Promises to deal with it. C# developers have async/await, which you're unfortunately not able to use because of the VS2010 restrain.

IMO,与 TPL 中的 JavaScript Promises 最接近的是 Stephen Toub 的 Then 模式.与 C# 4.0 中的 async/await 最接近的是他在同一博客文章中的 Iterate 模式,该模式使用 C# yield 功能.

IMO, the closest thing to the JavaScript Promises in TPL is Stephen Toub's Then pattern. And the closest thing to async/await in C# 4.0 is his Iterate pattern from the same blog post, which uses the C# yield feature.

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

Using the Iterate pattern, the above code could be rewritten in a more readable way. Note that inside GetThreePagesHelper you can use all the familiar synchronous code statements like using, for, while, try/catch etc. It is however important to understand the asynchronous code flow of this pattern:

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;
}

这篇关于使用异步调用时异常处理的良好模式的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆