使用异步调用时处理异常的好模式

本文关键字:异常 模式 处理 异步 调用 | 更新日期: 2023-09-27 17:49:15

我想使用一个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<string>,但GetAsync返回Task<HttpResponseMessage>。后者可能更有用,因为它提供了一个StatusCode .

你能分享一个关于如何正确使用异步调用和处理异常的模式吗?如能提供基本说明,则不胜感激。

使用异步调用时处理异常的好模式

对于成功和失败的场景,我不会使用单独的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<TResult>),你可以做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开发者称它为《Doom》的Callback Pyramid。他们承诺会解决这个问题。c#开发人员有async/await,不幸的是,由于VS2010的限制,您无法使用它。

在我看来,TPL中最接近JavaScript承诺的是Stephen Toub的Then模式。在c# 4.0中最接近async/await的是他的Iterate模式,来自同一篇博客文章,它使用了c# yield的特性。

使用Iterate模式,上面的代码可以以更可读的方式重写。请注意,在GetThreePagesHelper内部,您可以使用所有熟悉的同步代码语句,如using, for, while, try/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;
}