Ожидайте завершенную задачу, такую ​​же, как task.Result?

В настоящее время я читаю "Concurrency в кулинарной книге С#" Стивена Клири, и я заметил следующую технику:

var completedTask = await Task.WhenAny(downloadTask, timeoutTask);  
if (completedTask == timeoutTask)  
  return null;  
return await downloadTask;  

downloadTask - это вызов httpclient.GetStringAsync, а timeoutTask выполняет Task.Delay.

В случае отсутствия таймаута, downloadTask уже завершен. Зачем нужно делать второе ожидание, а не возвращать downloadTask.Result, учитывая, что задача уже завершена?

Ответ 1

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

Есть две причины, по которым я предпочитаю await более Result (или Wait). Во-первых, обработка ошибок отличается; await не завершает исключение в AggregateException. В идеале, асинхронный код никогда не должен иметь дело с AggregateException вообще, если он специально не хочет.

Вторая причина немного более тонкая. Как я описываю в своем блоге (и в книге), Result/Wait может вызвать взаимоблокировки, а может вызывают еще более тонкие взаимоблокировки при использовании в методе async. Итак, когда я читаю код, и я вижу Result или Wait, что является предупреждающим флагом. Result/Wait является правильным, если вы абсолютно уверены, что задача уже завершена. Мало того, что это трудно увидеть с первого взгляда (в коде реального мира), но он также более хрупкий для изменения кода.

Чтобы не сказать, что Result/Wait никогда не следует использовать. Я следую этим правилам в своем собственном коде:

  • Асинхронный код в приложении может использовать только await.
  • Асинхронный код утилиты (в библиотеке) может иногда использовать Result/Wait, если код действительно вызывает его. Такое использование должно иметь комментарии.
  • Параллельный код задачи может использовать Result и Wait.

Заметим, что (1), безусловно, является общим случаем, поэтому моя тенденция использовать await всюду и рассматривать другие случаи как исключения для общего правила.

Ответ 2

Это имеет смысл, если timeoutTask является произведением Task.Delay, которое я считаю тем, что есть в книге.

Task.WhenAny возвращает Task<Task>, где внутренняя задача является одной из тех, которые вы передали в качестве аргументов. Его можно было бы переписать следующим образом:

Task<Task> anyTask = Task.WhenAny(downloadTask, timeoutTask);
await anyTask;
if (anyTask.Result == timeoutTask)  
  return null;  
return downloadTask.Result; 

В любом случае, поскольку downloadTask уже завершен, существует очень незначительная разница между return await downloadTask и return downloadTask.Result. Он в том, что последний будет бросать AggregateException, который обертывает любое исходное исключение, как указано в комментариях @KirillShlenskiy. Первый будет просто перебрасывать исходное исключение.

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