Utilisation de méthodes asynchrones dans ASP.NET MVC 4

par Rick Anderson

Ce tutoriel vous apprend les bases de la création d’une application web asynchrone ASP.NET MVC à l’aide de Visual Studio Express 2012 for Web, qui est une version gratuite de Microsoft Visual Studio. Vous pouvez également utiliser Visual Studio 2012.

Un exemple complet est fourni pour ce tutoriel sur github https://github.com/RickAndMSFT/Async-ASP.NET/

La ASP.NET classe de contrôleur MVC 4 dans la combinaison .NET 4.5 vous permet d’écrire des méthodes d’action asynchrones qui retournent un objet de type Task<ActionResult>. Le .NET Framework 4 a introduit un concept de programmation asynchrone appelé Tâche et ASP.NET MVC 4 prend en charge Task. Les tâches sont représentées par le type de tâche et les types associés dans l’espace de noms System.Threading.Tasks . Le .NET Framework 4.5 s’appuie sur cette prise en charge asynchrone avec les mots clés await et async qui rendent l’utilisation des objets Task beaucoup moins complexe que les approches asynchrones précédentes. Le mot clé await est un raccourci syntaxique qui indique qu’un morceau de code doit attendre de façon asynchrone sur un autre élément de code. Le mot clé asynchrone représente un indicateur que vous pouvez utiliser pour marquer des méthodes en tant que méthodes asynchrones basées sur des tâches. La combinaison de l’objet await, async et Task vous permet d’écrire beaucoup plus facilement du code asynchrone dans .NET 4.5. Le nouveau modèle pour les méthodes asynchrones est appelé modèle asynchrone basé sur les tâches (TAP). Ce tutoriel part du principe que vous êtes familiarisé avec la programmation asynchrone à l’aide de mots clés await et async et de l’espace de noms Task .

Pour plus d’informations sur l’utilisation des mots clés await et async et l’espace de noms Task , consultez les références suivantes.

Mode de traitement des requêtes par le pool de threads

Sur le serveur web, le .NET Framework gère un pool de threads utilisés pour traiter ASP.NET requêtes. À l'arrivée d'une requête, un thread du pool est distribué pour la traiter. Si la demande est traitée de manière synchrone, le thread qui traite la demande est occupé pendant le traitement de la demande, et ce thread ne peut pas traiter une autre requête.

Cela peut ne pas être un problème, car le pool de threads peut être suffisamment grand pour accueillir de nombreux threads occupés. Toutefois, le nombre de threads dans le pool de threads est limité (la valeur maximale par défaut pour .NET 4.5 est de 5 000). Dans les applications volumineuses avec une concurrence élevée des requêtes de longue durée, tous les threads disponibles peuvent être occupés. Cet état est appelé privation de thread. Lorsque cette condition est atteinte, le serveur web met en file d’attente les demandes. Si la file d’attente des demandes est saturée, le serveur web rejette les requêtes avec une status HTTP 503 (Serveur trop occupé). Le pool de threads CLR présente des limitations sur les nouvelles injections de threads. Si l’accès concurrentiel est bursty (c’est-à-dire que votre site web peut soudainement obtenir un grand nombre de demandes) et que tous les threads de requête disponibles sont occupés en raison d’appels back-end avec une latence élevée, le taux d’injection de threads limité peut faire en sorte que votre application réponde très mal. En outre, chaque nouveau thread ajouté au pool de threads a une surcharge (par exemple, 1 Mo de mémoire de pile). Une application web utilisant des méthodes synchrones pour traiter les appels à latence élevée où le pool de threads atteint le maximum par défaut .NET 4.5 de 5 000 threads consomme environ 5 Go de mémoire de plus qu’une application capable de traiter les mêmes requêtes à l’aide de méthodes asynchrones et de seulement 50 threads. Lorsque vous effectuez un travail asynchrone, vous n’utilisez pas toujours un thread. Par exemple, lorsque vous effectuez une demande de service web asynchrone, ASP.NET n’utilise pas de threads entre l’appel de méthode asynchrone et l’objet await. L’utilisation du pool de threads pour traiter les demandes avec une latence élevée peut entraîner un encombrement de mémoire important et une utilisation médiocre du matériel serveur.

Traitement des requêtes asynchrones

Dans une application web qui voit un grand nombre de demandes simultanées au démarrage ou dont la charge est éclatée (où l’accès concurrentiel augmente soudainement), le fait de rendre les appels de service web asynchrones augmente la réactivité de l’application. Une requête asynchrone demande la même durée de traitement qu'une requête synchrone. Si une demande effectue un appel de service web qui nécessite deux secondes, elle prend deux secondes, qu’elle soit effectuée de manière synchrone ou asynchrone. Toutefois, pendant un appel asynchrone, un thread n’est pas bloqué pour répondre à d’autres requêtes pendant qu’il attend la fin de la première requête. Par conséquent, les requêtes asynchrones empêchent la mise en file d’attente des requêtes et la croissance du pool de threads quand de nombreuses demandes simultanées appellent des opérations de longue durée.

Choix de méthodes d'action synchrones ou asynchrones

Cette section répertorie des directives relatives aux situations dans lesquelles utiliser les méthodes d'action synchrones ou asynchrones. Ce ne sont que des directives; examinez chaque application individuellement pour déterminer si les méthodes asynchrones aident à améliorer les performances.

En général, utilisez des méthodes synchrones pour les conditions suivantes :

  • Les opérations sont simples ou à durée d'exécution courte.
  • La simplicité est plus importante que l'efficacité.
  • Les opérations sont essentiellement des opérations UC, et non des opérations qui impliquent une surcharge du disque ou du réseau. L'utilisation de méthodes d'action asynchrones sur des opérations utilisant le processeur de manière intensive ne présente aucun avantage et donne lieu à une surcharge plus importante.

En général, utilisez des méthodes asynchrones pour les conditions suivantes :

  • Vous appelez des services qui peuvent être consommés via des méthodes asynchrones et vous utilisez .NET 4.5 ou version ultérieure.
  • Les opérations utilisent le réseau ou les E/S de manière intensive et non le processeur.
  • Le parallélisme est plus important que la simplicité du code.
  • Vous souhaitez fournir un mécanisme qui permet aux utilisateurs d'annuler une requête à durée d'exécution longue.
  • Lorsque l’avantage du basculement de threads l’emporte sur le coût du commutateur de contexte. En général, vous devez rendre une méthode asynchrone si la méthode synchrone attend sur le thread de requête ASP.NET sans effectuer de travail. En rendant l’appel asynchrone, le thread de demande de ASP.NET n’est pas bloqué pendant qu’il attend la fin de la demande de service web.
  • Les tests montrent que les opérations de blocage constituent un goulot d’étranglement dans les performances du site et qu’IIS peut traiter davantage de demandes à l’aide de méthodes asynchrones pour ces appels bloquants.

L'exemple téléchargeable indique comment utiliser efficacement des méthodes d'action asynchrones. L’exemple fourni a été conçu pour fournir une démonstration simple de la programmation asynchrone dans ASP.NET MVC 4 à l’aide de .NET 4.5. L’exemple n’est pas destiné à être une architecture de référence pour la programmation asynchrone dans ASP.NET MVC. L’exemple de programme appelle API Web ASP.NET méthodes qui, à leur tour, appellent Task.Delay pour simuler des appels de service web de longue durée. La plupart des applications de production ne présentent pas de tels avantages évidents à l’utilisation de méthodes d’action asynchrones.

Peu d'applications requièrent que toutes leurs méthodes d'action soient asynchrones. Souvent, la conversion de quelques méthodes d'action synchrones en méthodes asynchrones offre le meilleur rapport performances/travail requis.

Exemple d’application

Vous pouvez télécharger l’exemple d’application à partir du https://github.com/RickAndMSFT/Async-ASP.NET/ site GitHub . Le dépôt se compose de trois projets :

  • Mvc4Async : ASP.NET projet MVC 4 qui contient le code utilisé dans ce didacticiel. Il effectue des appels d’API web au service WebAPIpgw .
  • WebAPIpgw : ASP.NET projet d’API web MVC 4 qui implémente les Products, Gizmos and Widgets contrôleurs. Il fournit les données du projet WebAppAsync et du projet Mvc4Async .
  • WebAppAsync : projet ASP.NET Web Forms utilisé dans un autre didacticiel.

Méthode d’action synchrone Gizmos

Le code suivant montre la Gizmos méthode d’action synchrone utilisée pour afficher une liste de gizmos. (Pour cet article, un gizmo est un dispositif mécanique fictif.)

public ActionResult Gizmos()
{
    ViewBag.SyncOrAsync = "Synchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos", gizmoService.GetGizmos());
}

Le code suivant montre la GetGizmos méthode du service gizmo.

public class GizmoService
{
    public async Task<List<Gizmo>> GetGizmosAsync(
        // Implementation removed.
       
    public List<Gizmo> GetGizmos()
    {
        var uri = Util.getServiceUri("Gizmos");
        using (WebClient webClient = new WebClient())
        {
            return JsonConvert.DeserializeObject<List<Gizmo>>(
                webClient.DownloadString(uri)
            );
        }
    }
}

La GizmoService GetGizmos méthode transmet un URI à un service HTTP API Web ASP.NET qui retourne une liste de données gizmos. Le projet WebAPIpgw contient l’implémentation de l’API gizmos, widget web et product des contrôleurs.
L’image suivante montre la vue gizmos de l’exemple de projet.

Gadgets

Création d’une méthode d’action gizmos asynchrone

L’exemple utilise les nouveaux mots clés asynchrones et await (disponibles dans .NET 4.5 et Visual Studio 2012) pour permettre au compilateur de gérer les transformations complexes nécessaires à la programmation asynchrone. Le compilateur vous permet d’écrire du code à l’aide des constructions de flux de contrôle synchrones du C#, et le compilateur applique automatiquement les transformations nécessaires pour utiliser les rappels afin d’éviter de bloquer les threads.

Le code suivant montre la Gizmos méthode synchrone et la GizmosAsync méthode asynchrone. Si votre navigateur prend en charge l’élément HTML 5<mark>, les modifications s’affichent en GizmosAsync surbrillance jaune.

public ActionResult Gizmos()
{
    ViewBag.SyncOrAsync = "Synchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos", gizmoService.GetGizmos());
}
public async Task<ActionResult> GizmosAsync()
{
    ViewBag.SyncOrAsync = "Asynchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos", await gizmoService.GetGizmosAsync());
}

Les modifications suivantes ont été appliquées pour permettre à l’objet GizmosAsync d’être asynchrone.

  • La méthode est marquée avec le mot clé asynchrone, qui indique au compilateur de générer des rappels pour des parties du corps et de créer automatiquement un Task<ActionResult> qui est retourné.
  • « Async » a été ajouté au nom de la méthode. L’ajout de « Async » n’est pas obligatoire, mais est la convention lors de l’écriture de méthodes asynchrones.
  • Le type de retour a été remplacé par ActionResultTask<ActionResult>. Le type de retour de Task<ActionResult> représente le travail en cours et fournit aux appelants de la méthode un handle par lequel attendre la fin de l’opération asynchrone. Dans ce cas, l’appelant est le service web. Task<ActionResult> représente un travail continu avec un résultat de ActionResult.
  • La mot clé await a été appliquée à l’appel de service web.
  • L’API de service web asynchrone a été appelée (GetGizmosAsync).

À l’intérieur du corps de la GetGizmosAsync méthode, GetGizmosAsync une autre méthode asynchrone est appelée. GetGizmosAsync retourne immédiatement un Task<List<Gizmo>> qui finira par se terminer lorsque les données seront disponibles. Étant donné que vous ne souhaitez rien faire d’autre tant que vous n’avez pas les données gizmo, le code attend la tâche (à l’aide de l’mot clé await). Vous pouvez utiliser le mot clé await uniquement dans les méthodes annotées avec le mot clé asynchrone.

Le mot clé await ne bloque pas le thread tant que la tâche n’est pas terminée. Il inscrit le reste de la méthode en tant que rappel sur la tâche et retourne immédiatement. Une fois la tâche attendue terminée, elle appelle ce rappel et reprend donc l’exécution de la méthode là où elle s’est arrêté. Pour plus d’informations sur l’utilisation des mots clés await et async et de l’espace de noms Task , consultez les références asynchrones.

Le code suivant représente les méthodes GetGizmos et GetGizmosAsync.

public List<Gizmo> GetGizmos()
{
    var uri = Util.getServiceUri("Gizmos");
    using (WebClient webClient = new WebClient())
    {
        return JsonConvert.DeserializeObject<List<Gizmo>>(
            webClient.DownloadString(uri)
        );
    }
}
public async Task<List<Gizmo>> GetGizmosAsync()
{
    var uri = Util.getServiceUri("Gizmos");
    using (HttpClient httpClient = new HttpClient())
    {
        var response = await httpClient.GetAsync(uri);
        return (await response.Content.ReadAsAsync<List<Gizmo>>());
    }
}

Les modifications asynchrones sont similaires à celles apportées à GizmosAsync ci-dessus.

  • La signature de méthode a été annotée avec le mot clé asynchrone, le type de retour a été remplacé par Task<List<Gizmo>>et Async a été ajouté au nom de la méthode.
  • La classe HttpClient asynchrone est utilisée à la place de la classe WebClient .
  • Le mot clé await a été appliqué aux méthodes asynchrones HttpClient.

L’image suivante montre la vue gizmo asynchrone.

Async

La présentation des navigateurs des données de gizmos est identique à la vue créée par l’appel synchrone. La seule différence est que la version asynchrone peut être plus performante sous des charges lourdes.

Exécution de plusieurs opérations en parallèle

Les méthodes d’action asynchrones présentent un avantage significatif par rapport aux méthodes synchrones lorsqu’une action doit effectuer plusieurs opérations indépendantes. Dans l’exemple fourni, la méthode PWGsynchrone (pour Products, Widgets et Gizmos) affiche les résultats de trois appels de service web pour obtenir une liste de produits, widgets et gizmos. Le projet API Web ASP.NET qui fournit ces services utilise Task.Delay pour simuler la latence ou ralentir les appels réseau. Lorsque le délai est défini sur 500 millisecondes, la méthode asynchrone PWGasync prend un peu plus de 500 millisecondes, tandis que la version synchrone PWG prend plus de 1 500 millisecondes. La méthode synchrone PWG est illustrée dans le code suivant.

public ActionResult PWG()
{
    ViewBag.SyncType = "Synchronous";
    var widgetService = new WidgetService();
    var prodService = new ProductService();
    var gizmoService = new GizmoService();

    var pwgVM = new ProdGizWidgetVM(
        widgetService.GetWidgets(),
        prodService.GetProducts(),
        gizmoService.GetGizmos()
       );

    return View("PWG", pwgVM);
}

La méthode asynchrone PWGasync est illustrée dans le code suivant.

public async Task<ActionResult> PWGasync()
{
    ViewBag.SyncType = "Asynchronous";
    var widgetService = new WidgetService();
    var prodService = new ProductService();
    var gizmoService = new GizmoService();

    var widgetTask = widgetService.GetWidgetsAsync();
    var prodTask = prodService.GetProductsAsync();
    var gizmoTask = gizmoService.GetGizmosAsync();

    await Task.WhenAll(widgetTask, prodTask, gizmoTask);

    var pwgVM = new ProdGizWidgetVM(
       widgetTask.Result,
       prodTask.Result,
       gizmoTask.Result
       );

    return View("PWG", pwgVM);
}

L’image suivante montre la vue retournée par la méthode PWGasync .

pwgAsync

Utilisation d’un jeton d’annulation

Les méthodes d’action asynchrone retournées Task<ActionResult>sont annulables, c’est-à-dire qu’elles prennent un paramètre CancellationToken quand l’attribut AsyncTimeout est fourni. Le code suivant montre la GizmosCancelAsync méthode avec un délai d’expiration de 150 millisecondes.

[AsyncTimeout(150)]
[HandleError(ExceptionType = typeof(TimeoutException),
                                    View = "TimeoutError")]
public async Task<ActionResult> GizmosCancelAsync(
                       CancellationToken cancellationToken )
{
    ViewBag.SyncOrAsync = "Asynchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos",
        await gizmoService.GetGizmosAsync(cancellationToken));
}

Le code suivant montre la surcharge GetGizmosAsync, qui prend un paramètre CancellationToken .

public async Task<List<Gizmo>> GetGizmosAsync(string uri,
    CancellationToken cancelToken = default(CancellationToken))
{
    using (HttpClient httpClient = new HttpClient())
    {
        var response = await httpClient.GetAsync(uri, cancelToken);
        return (await response.Content.ReadAsAsync<List<Gizmo>>());
    }
}

Dans l’exemple d’application fourni, la sélection du lien Démonstration du jeton d’annulation appelle la GizmosCancelAsync méthode et illustre l’annulation de l’appel asynchrone.

Configuration du serveur pour les appels de service web à haute concurrence/latence élevée

Pour tirer parti des avantages d’une application web asynchrone, vous devrez peut-être apporter des modifications à la configuration du serveur par défaut. Gardez à l’esprit les points suivants lors de la configuration et du test de contrainte de votre application web asynchrone.

  • Windows 7, Windows Vista et tous les systèmes d’exploitation clients Windows ont un maximum de 10 demandes simultanées. Vous aurez besoin d’un système d’exploitation Windows Server pour voir les avantages des méthodes asynchrones sous une charge élevée.

  • Inscrivez .NET 4.5 auprès d’IIS à partir d’une invite de commandes avec élévation de privilèges :
    %windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_regiis -i
    Consultez ASP.NET’outil d’inscription IIS (Aspnet_regiis.exe)

  • Vous devrez peut-être augmenter la limite de file d’attente deHTTP.sys de la valeur par défaut de 1 000 à 5 000. Si le paramètre est trop faible, vous pouvez voir HTTP.sys rejeter des demandes avec un status HTTP 503. Pour modifier la limite de file d’attente HTTP.sys :

    • Ouvrez le Gestionnaire IIS et accédez au volet Pools d’applications.
    • Cliquez avec le bouton droit sur le pool d’applications cible et sélectionnez Paramètres avancés.
      Avancé
    • Dans la boîte de dialogue Paramètres avancés , modifiez la longueur de la file d’attente de 1 000 à 5 000.
      Longueur de la file d’attente

    Notez que dans les images ci-dessus, le .NET Framework est répertorié sous la forme v4.0, même si le pool d’applications utilise .NET 4.5. Pour comprendre cette différence, consultez les rubriques suivantes :

  • Si votre application utilise des services web ou des System.NET pour communiquer avec un serveur principal via HTTP, vous devrez peut-être augmenter l’élément connectionManagement/maxconnection . Pour les applications ASP.NET, la fonctionnalité de configuration automatique est limitée à 12 fois le nombre de processeurs. Cela signifie que sur un quad-proc, vous pouvez avoir au maximum 12 * 4 = 48 connexions simultanées à un point de terminaison IP. Étant donné que cela est lié à autoConfig, le moyen le plus simple d’augmenter maxconnection dans une application ASP.NET consiste à définir System.Net.ServicePointManager.DefaultConnectionLimit par programmation dans la méthode from Application_Start dans le fichier global.asax . Consultez l’exemple de téléchargement pour obtenir un exemple.

  • Dans .NET 4.5, la valeur par défaut 5000 pour MaxConcurrentRequestsPerCPU doit être correcte.