web-dev-qa-db-fra.com

L'async / wait convient-il aux méthodes qui sont à la fois IO et liées au CPU?

La documentation MSDN semble indiquer que async et await conviennent aux tâches liées aux E/S tandis que Task.Run doit être utilisé pour les tâches liées au processeur.

Je travaille sur une application qui exécute des requêtes HTTP pour récupérer des documents HTML, qu'elle analyse ensuite. J'ai une méthode qui ressemble à ceci:

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
}

Est-ce une bonne utilisation appropriée de async et await, ou est-ce que je l'utilise de manière excessive?

41
Sam

Il y a déjà deux bonnes réponses, mais pour ajouter mon 0.02 ...

Si vous parlez de consommer des opérations asynchrones, async/await fonctionne parfaitement pour les deux E/S liées et lié au processeur.

Je pense que les documents MSDN ont une légère inclinaison vers la production d'opérations asynchrones, auquel cas vous voulez utiliser TaskCompletionSource (ou similaire ) pour les E/S et Task.Run (ou similaire) pour le processeur. Une fois que vous avez créé le wrapper initial de Task, il est préférable consommé par async et await .

Pour votre exemple particulier, cela dépend vraiment du temps que prendra LoadHtmlDocument. Si vous supprimez le Task.Run, vous l'exécuterez dans le même contexte qui appelle LoadPage (éventuellement sur un thread d'interface utilisateur). Les directives de Windows 8 spécifient que toute opération prenant plus de 50 ms doit être effectuée async... en gardant à l'esprit que 50 ms sur votre machine de développeur peuvent être plus longues sur la machine d'un client ...

Donc, si vous pouvez garantir que LoadHtmlDocument fonctionnera pendant moins de 50 ms, vous pouvez simplement l'exécuter directement:

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
}

Cependant, je recommanderais ConfigureAwait comme @svick l'a mentionné:

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
}

Avec ConfigureAwait, si la requête HTTP ne se termine pas immédiatement (de manière synchrone), cela entraînera (dans ce cas) LoadHtmlDocument à être exécuté sur un thread de pool de threads sans appel explicite à Task.Run.

Si vous êtes intéressé par les performances de async à ce niveau, vous devriez consulter vidéo et article MSDN de Stephen Toub sur le sujet. Il a des tonnes d'informations utiles.

41
Stephen Cleary

Il convient à await toute opération asynchrone (c'est-à-dire représentée par un Task).

Le point clé est que pour les opérations IO, dans la mesure du possible, vous souhaitez utiliser une méthode fournie qui est, à la base, asynchrone, plutôt que d'utiliser Task.Run sur une méthode synchrone bloquante. Si vous bloquez un thread (même un thread de pool de threads) lors de l'exécution d'E/S, vous ne tirez pas parti de la puissance réelle du modèle await.

Une fois que vous avez créé un Task qui représente votre opération, vous ne vous souciez plus si c'est un processeur ou IO lié. Pour l'appelant, c'est juste une opération asynchrone qui doit être await- ed.

20
Servy

Il y a plusieurs choses à considérer:

  • Dans une application GUI, vous souhaitez que le moins de code possible soit exécuté sur le thread d'interface utilisateur. Dans ce cas, décharger une opération liée à l'UC vers un autre thread en utilisant Task.Run() est probablement une bonne idée. Bien que les utilisateurs de votre code puissent le faire eux-mêmes, s'ils le souhaitent.
  • Dans quelque chose comme l'application ASP.NET, il n'y a pas de thread d'interface utilisateur et vous ne vous souciez que des performances. Dans ce cas, l'utilisation de Task.Run() nécessite de la surcharge au lieu d'exécuter le code directement, mais cela ne devrait pas être significatif si l'opération prend un certain temps. (De plus, le retour au contexte de synchronisation entraîne des frais supplémentaires, ce qui est une raison supplémentaire pour laquelle vous devez utiliser ConfigureAwait(false) pour la plupart des await dans votre code de bibliothèque.)
  • Si votre méthode est asynchrone (dont BTW devrait également être reflété dans le nom de la méthode, pas seulement son type de retour), les gens s'attendront à ce qu'elle ne bloque pas le thread de contexte de synchronisation, même pour le travail lié au CPU.

Je pense que l'utilisation de await Task.Run() est le bon choix ici. Il présente certains frais généraux, mais aussi certains avantages, qui peuvent être importants.

18
svick