Хороший шаблон для обработки исключений при использовании асинхронных вызовов

Я хочу использовать веб-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). Я заметил, что у "Task" есть свойство IsFaulted и свойство AggregateException, которые, возможно, можно было бы использовать, но я пока не уверен, как это сделать.

Другое наблюдение заключалось в том, что GetStringAsync возвращает Task<string>, а GetAsync возвращает Task<HttpResponseMessage>. Последнее может быть более полезным, так как оно представляет собой StatusCode.

Не могли бы вы поделиться шаблоном того, как правильно использовать асинхронные вызовы и хорошо обрабатывать исключения? Основное объяснение также будет оценено.


person Learner    schedule 08.03.2014    source источник


Ответы (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<TResult>), вы можете сделать t.GetAwaiter().GetResult(), чтобы повторно создать исходное исключение внутри лямбды ContinueWith; 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());
}

Любые исключения, созданные внутри внутренних задач, будут распространяться на самую внешнюю ContinueWith лямбду, когда вы получаете доступ к результатам внутренних задач (taskN.Result).

Этот код функционален, но он уродлив и нечитаем. Разработчики JavaScript называют это Пирамида обратного вызова судьбы. У них есть Promises, чтобы справиться с этим. У разработчиков C# есть async/await, который вы, к сожалению, не можете использовать из-за ограничения VS2010.

ИМО, ближе всего к обещаниям JavaScript в TPL находится Стивен. Выкройка Then Туба. И ближе всего к async/await в C# 4.0 его шаблон 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;
}
person noseratio    schedule 10.03.2014
comment
Еще раз спасибо за этот отличный и полный ответ. Насколько я вижу, в вашем коде так много деталей, которых я не понимаю... и бедный я, я всего лишь новичок. Я должен признать, что мне довольно сложно понять все эти вещи TPL... особенно в мире VS2010. Очень странно, что я не отказался от этого раньше и не выбрал что-то более простое для моего мира, например HttpWebRequest :) ... о боже, трудное решение - person Learner; 10.03.2014
comment
@Cristi, использование HttpWebRequest не будет иметь большого значения. То есть вы можете просто сделать var text = httpClient.GetStringAsync("http://example.net").Result, что будет блокирующим синхронным вызовом (из-за Result). Не стесняйтесь разъяснять то, что неясно здесь или в отдельном вопросе. Кроме того, попробуйте выполнить этот код в отладчике, это может помочь понять его. - person noseratio; 10.03.2014
comment
Вы определенно правы! Один небольшой вопрос: почему мой код из вопроса не выдает никаких исключений, если URL-адрес недоступен? - person Learner; 10.03.2014
comment
Кстати, я думаю, что в худшем случае я мог бы выбрать javaScript/jQuery вместо приложения WinForms, но давайте посмотрим... Пока я бы предпочел настольное приложение. - person Learner; 10.03.2014
comment
Хорошо, я нашел ответ на свой предыдущий вопрос: stackoverflow.com/questions/17151215/ - person Learner; 11.03.2014
comment
@Cristi, обработка исключений в .NET 4.0 работает немного иначе, чем в версии 4.5, проверьте Задачи и необработанные исключения. - person noseratio; 11.03.2014