Подходит ли async/await для методов, связанных с IO и CPU?

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

Я работаю над приложением, которое выполняет 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, или я слишком его использую?

3 ответов


уже есть два хороших ответа, но добавить мой 0.02...

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

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

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

так что если вы можете гарантировать, что 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 производительность на этом уровне, вы должны проверить видео и статья MSDN на эту тему. У него куча полезной информации.


есть несколько вещей, чтобы рассмотреть:

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

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


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

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

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