Как выполнять несколько задач, обрабатывать исключения и при этом возвращать результаты

Я обновляю свой набор навыков параллелизма. Моя проблема кажется довольно распространенной: читать из нескольких Uris, анализировать и работать с результatom и т.д. У меня есть параллелизм в С# Cookbook. Есть несколько примеров использования GetStringAsync, таких как

static async Task<string> DownloadAllAsync(IEnumerable<string> urls)
{
    var httpClient = new HttpClient();

    var downloads = urls.Select(url => httpClient.GetStringAsync(url));

    Task<string>[] downloadTasks = downloads.ToArray();

    string[] htmlPages = await Task.WhenAll(downloadTasks);

    return string.Concat(htmlPages);
}

Мне нужен асинхронный шаблон для запуска нескольких асинхронных задач, захвата полного или частичного успеха.

  1. URL 1 успешен
  2. URL 2 успешен
  3. Сбой URL-адреса 3 (тайм-аут, неверный формат Uri, 401 и т.д.)
  4. URL 4 успешен
  5. ... еще 20 со смешанным успехом

ожидание задачи DownloadAllAsync вызовет единственное статистическое исключение, если произойдет сбой, отбросив накопленные результаты. Из моего ограниченного исследования, когда WhenAll или WaitAll ведут себя одинаково. Я хочу перехватывать исключения, регистрировать сбои, но продолжать работу с оставшимися задачами, даже если все они терпят неудачу. Я мог бы обрабатывать их один за другим, но разве это не противоречит цели, позволяющей TPL управлять всем процессом? Есть ли ссылка на шаблон, который будет выполнять это простым способом TPL? Возможно, я использую не тот инструмент?

Ответ 1

  Я хочу перехватывать исключения, регистрировать сбои, но продолжаю выполнять оставшиеся задачи, даже если они все не срабатывают.

В этом случае самое чистое решение - изменить то, что ваш код делает для каждого элемента. Т.е. этот текущий код:

var downloads = urls.Select(url => httpClient.GetStringAsync(url));

говорит "для каждого URL, загрузите строку". То, что вы хотите сказать, это "для каждого URL, загрузите строку, а затем войдите и игнорируйте все ошибки":

static async Task<string> DownloadAllAsync(IEnumerable<string> urls)
{
  var httpClient = new HttpClient();
  var downloads = urls.Select(url => TryDownloadAsync(httpClient, url));
  Task<string>[] downloadTasks = downloads.ToArray();
  string[] htmlPages = await Task.WhenAll(downloadTasks);
  return string.Concat(htmlPages);
}

static async Task<string> TryDownloadAsync(HttpClient client, string url)
{
  try
  {
    return await client.GetStringAsync(url);
  }
  catch (Exception ex)
  {
    Log(ex);
    return string.Empty; // or whatever you prefer
  }
}

Ответ 2

Вы можете прикрепить продолжение для всех своих задач и ждать их, а не ждать непосредственно от задач.

static async Task<string> DownloadAllAsync(IEnumerable<string> urls)
{
    var httpClient = new HttpClient();

    IEnumerable<Task<Task<string>>> downloads = urls.Select(url => httpClient.GetStringAsync(url).ContinueWith(p=> p, TaskContinuationOptions.ExecuteSynchronously));

    Task<Task<string>>[] downloadTasks = downloads.ToArray();

    Task<string>[] compleTasks =  await Task.WhenAll(downloadTasks);

    foreach (var task in compleTasks)
    {
        if (task.IsFaulted)//Or task.IsCanceled
        {
            //Handle it
        }
    }

    var htmlPages = compleTasks.Where(x => x.Status == TaskStatus.RanToCompletion)
        .Select(x => x.Result);

    return string.Concat(htmlPages);
}

Это не прекратится, как только одна задача завершится неудачей, скорее она будет ждать завершения всех задач. Затем обработайте успех и неудачу отдельно.