Подтвердить что ты не робот

Является ли async/await подходящим для методов, которые связаны как с IO, так и с CPU?

В документации MSDN указано, что async и await подходят для задач с привязкой к IO, тогда как Task.Run следует использовать для задач, связанных с ЦП.

Я работаю над приложением, которое выполняет HTTP-запросы для извлечения HTML-документов, которые затем анализирует. У меня есть метод, который выглядит так:

public async Task<HtmlDocument> LoadPage(Uri address)
{
    using (var httpResponse = await new HttpClient().GetAsync(address)) //IO-bound
    using (var responseContent = httpResponse.Content)
    using (var contentStream = await responseContent.ReadAsStreamAsync())
        return await Task.Run(() => LoadHtmlDocument(contentStream)); //CPU-bound
}

Является ли это хорошим и подходящим использованием async и await, или я его использую?

4b9b3361

Ответ 1

Уже есть два хороших ответа, но добавьте мои 0,02...

Если вы говорите об использовании асинхронных операций, async/await отлично работает как для привязки ввода-вывода, так и для привязки к ЦП.

Я думаю, что документы MSDN имеют небольшой уклон в сторону создания асинхронных операций, и в этом случае вы хотите использовать TaskCompletionSource (или аналогичный) для I/O-bound и Task.Run (или подобных) для CPU- связаны. Создав исходную оболочку Task, она лучше всего используется async и await.

В вашем конкретном примере это действительно сводится к тому, сколько времени займет LoadHtmlDocument. Если вы удалите Task.Run, вы выполните его в том же контексте, который вызывает LoadPage (возможно, в потоке пользовательского интерфейса). В руководствах Windows 8 указывается, что любая операция, занимающая более 50 мс, должна быть сделана async... с учетом того, что 50 мс на вашей машине разработчика могут быть более длинными на клиентской машине...

Итак, если вы можете гарантировать, что LoadHtmlDocument будет работать менее 50 мс, вы можете просто выполнить его непосредственно:

public async Task<HtmlDocument> LoadPage(Uri address)
{
  using (var httpResponse = await new HttpClient().GetAsync(address)) //IO-bound
  using (var responseContent = httpResponse.Content)
  using (var contentStream = await responseContent.ReadAsStreamAsync()) //IO-bound
    return LoadHtmlDocument(contentStream); //CPU-bound
}

Однако я бы порекомендовал ConfigureAwait, как сказал @svick:

public async Task<HtmlDocument> LoadPage(Uri address)
{
  using (var httpResponse = await new HttpClient().GetAsync(address)
      .ConfigureAwait(continueOnCapturedContext: false)) //IO-bound
  using (var responseContent = httpResponse.Content)
  using (var contentStream = await responseContent.ReadAsStreamAsync()
      .ConfigureAwait(continueOnCapturedContext: false)) //IO-bound
    return LoadHtmlDocument(contentStream); //CPU-bound
}

С ConfigureAwait, если HTTP-запрос не завершится немедленно (синхронно), тогда это (в этом случае) приведет к тому, что LoadHtmlDocument будет выполняться в потоке пула потоков без явного вызова Task.Run.

Если вас интересует производительность async на этом уровне, вы должны проверить Stephen Toub видео и статья MSDN по этому вопросу. У него много полезной информации.

Ответ 2

Можно рассмотреть несколько вещей:

  • В приложении GUI вы хотите как можно меньше кода выполнить в потоке пользовательского интерфейса. В этом случае перегрузка операции с привязкой к другому процессору с использованием Task.Run(), вероятно, является хорошей идеей. Хотя пользователи вашего кода могут сделать это сами, если они захотят.
  • В чем-то вроде приложения ASP.NET нет потока пользовательского интерфейса, и все, о чем вы заботитесь, это производительность. В этом случае есть некоторые накладные расходы при использовании Task.Run() вместо прямого запуска кода, но это не должно быть значительным, если операция на самом деле занимает некоторое время. (Кроме того, есть некоторые накладные расходы при возврате в контекст синхронизации, что является еще одной причиной, по которой вы должны использовать ConfigureAwait(false) для большинства await в вашем библиотечном коде.)
  • Если ваш метод isync (какой BTW должен также отражаться на имени метода, а не только на его возвращаемом типе), люди будут ожидать, что он не будет блокировать поток контекста синхронизации, даже для работы с ЦП.

Взвешивание, что, думаю, использование await Task.Run() является правильным выбором здесь. У этого есть некоторые накладные расходы, но также и некоторые преимущества, которые могут быть значительными.

Ответ 3

Соответственно await любая операция, которая является асинхронной (т.е. представлена ​​ Task).

Ключевым моментом является то, что для операций ввода-вывода, когда это возможно, вы хотите использовать предоставленный метод, который по своей сути является асинхронным, а не использует Task.Run для синхронного метода блокировки. Если вы блокируете поток (даже поток потока потоков) при выполнении IO, вы не используете реальную мощность модели await.

Как только вы создали Task, который представляет вашу операцию, вам все равно, не связано ли это с CPU или IO. Для вызывающего абонента это просто некоторая операция async, которая должна быть await -ed.