Utilisation de méthodes asynchrones dans ASP.NET 4.5

par Rick Anderson

Ce tutoriel vous apprendra les principes de base de la création d’une application ASP.NET Web Forms asynchrone à l’aide de Visual Studio Express 2012 pour Web, qui est une version gratuite de Microsoft Visual Studio. Vous pouvez également utiliser Visual Studio 2012. Les sections suivantes sont incluses dans ce didacticiel.

Un exemple complet est fourni pour ce didacticiel à l’adresse
https://github.com/RickAndMSFT/Async-ASP.NET/ sur le site GitHub .

ASP.NET pages web 4.5 en combinaison .NET 4.5 vous permet d’inscrire des méthodes asynchrones qui retournent un objet de type Task. Le .NET Framework 4 a introduit un concept de programmation asynchrone appelé tâche et ASP.NET 4.5 prend en charge la tâche. 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ésawait et asynchrones qui permettent d’utiliser des objets Task beaucoup moins complexes que les approches asynchrones précédentes. Le mot clé await est un raccourci syntactique pour indiquer qu’un morceau de code doit attendre de manière asynchrone sur un autre élément de code. Le mot clé asynchrone représente un indicateur que vous pouvez utiliser pour marquer les méthodes en tant que méthodes asynchrones basées sur des tâches. La combinaison de l’objet Await, async et Task facilite l’écriture de 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 didacticiel suppose que vous connaissez un programme asynchrone à l’aide de mots clés await et asynchrones et de l’espace de noms De tâche .

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

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

Sur le serveur web, .NET Framework gère un pool de threads qui sont utilisés pour effectuer des demandes de ASP.NET de service. À l'arrivée d'une requête, un thread du pool est distribué pour la traiter. Si la requête est traitée de manière synchrone, le thread qui traite la requête est occupé pendant le traitement de la requête et que 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 volumineux pour prendre en charge de nombreux threads occupés. Toutefois, le nombre de threads dans le pool de threads est limité (le maximum par défaut pour .NET 4.5 est de 5 000). Dans les applications volumineuses avec une concurrence élevée des demandes 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 de requêtes devient complète, le serveur web rejette les demandes avec un état HTTP 503 (Serveur trop occupé). Le pool de threads CLR a des limitations sur les nouvelles injections de threads. Si l’accès concurrentiel est en rafale (autrement dit, votre site web peut soudainement obtenir un grand nombre de requêtes) et tous les threads de requête disponibles sont occupés en raison des appels principaux avec une latence élevée, le taux d’injection de threads limité peut rendre votre application répondre 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 des appels à latence élevée où le pool de threads augmente vers le maximum par défaut .NET 4.5 de 5, 000 threads consomment environ 5 Go de mémoire que l’application pouvant le service les mêmes requêtes à l’aide de méthodes asynchrones et seulement 50 threads. Lorsque vous effectuez un travail asynchrone, vous n’utilisez pas toujours un thread. Par exemple, lorsque vous effectuez une requête de service web asynchrone, ASP.NET n’utilise aucun thread entre l’appel de méthode asynchrone et l’attente. L’utilisation du pool de threads pour les demandes de service avec une latence élevée peut entraîner une empreinte mémoire importante et une mauvaise utilisation du matériel serveur.

Traitement des requêtes asynchrones

Dans les applications web qui voient un grand nombre de requêtes simultanées au démarrage ou ont une charge en rafale (où la concurrence augmente soudainement), l’exécution d’appels de service web asynchrone augmente la réactivité de votre application. Une requête asynchrone demande la même durée de traitement qu'une requête synchrone. Par exemple, si une requête effectue un appel de service web qui nécessite deux secondes pour se terminer, la requête prend deux secondes, qu’elle soit effectuée de façon synchrone ou asynchrone. Toutefois, lors d’un appel asynchrone, un thread n’est pas bloqué pour répondre à d’autres demandes 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 lorsqu’il existe de nombreuses requêtes simultanées qui appellent des opérations de longue durée.

Choix des méthodes synchrones ou asynchrones

Cette section répertorie les instructions relatives au moment d’utiliser des méthodes synchrones ou asynchrones. Il s’agit simplement de lignes directrices; examinez chaque application individuellement pour déterminer si les méthodes asynchrones aident 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 asynchrones sur les opérations liées au processeur ne offre aucun avantage et entraîne 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 une 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 de changer de threads dépasse le coût du commutateur de contexte. En général, vous devez effectuer une méthode asynchrone si la méthode synchrone bloque le thread de requête ASP.NET tout en n’effectuant aucun travail. En effectuant l’appel asynchrone, le thread de requête ASP.NET n’est pas bloqué pendant qu’il attend la fin de la demande de service web.

  • Le test montre que les opérations de blocage sont un goulot d’étranglement dans les performances du site et que IIS peut traiter davantage de requêtes à l’aide de méthodes asynchrones pour ces appels bloquants.

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

Peu d’applications nécessitent que toutes les méthodes soient asynchrones. Souvent, la conversion de quelques méthodes synchrones en méthodes asynchrones offre la meilleure efficacité pour la quantité de travail requise.

Exemple d’application

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

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

Page synchrone de Gizmos

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

public partial class Gizmos : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        var gizmoService = new GizmoService();
        GizmoGridView.DataSource = gizmoService.GetGizmos();
        GizmoGridView.DataBind();
    }
}

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 de gizmos. Le projet WebAPIpgw contient l’implémentation de l’API gizmos, widget web et product des contrôleurs.
L’image suivante montre la page gizmos de l’exemple de projet.

Capture d’écran de la page de navigateur web Sync Gizmos montrant la table des gizmos avec des détails correspondants comme entrés dans les contrôleurs d’API web.

Création d’une page Gizmos asynchrone

L’exemple utilise les nouveaux mots clés async et await (disponibles dans .NET 4.5 et Visual Studio 2012) pour permettre au compilateur de gérer les transformations compliquées 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.

ASP.NET pages asynchrones doivent inclure la directive Page avec l’attribut Async défini sur « true ». Le code suivant montre la directive Page avec l’attribut Async défini sur « true » pour la page GizmosAsync.aspx .

<%@ Page Async="true"  Language="C#" AutoEventWireup="true" 
    CodeBehind="GizmosAsync.aspx.cs" Inherits="WebAppAsync.GizmosAsync" %>

Le code suivant montre la Gizmos méthode synchrone Page_Load et la GizmosAsync page asynchrone. Si votre navigateur prend en charge l’élément de marque> HTML 5<, vous verrez les modifications apportées en GizmosAsync surbrillance jaune.

protected void Page_Load(object sender, EventArgs e)
{
   var gizmoService = new GizmoService();
   GizmoGridView.DataSource = gizmoService.GetGizmos();
   GizmoGridView.DataBind();
}

Version asynchrone :

protected void Page_Load(object sender, EventArgs e)
{
    RegisterAsyncTask(new PageAsyncTask(GetGizmosSvcAsync));
}

private async Task GetGizmosSvcAsync()
{
    var gizmoService = new GizmoService();
    GizmosGridView.DataSource = await gizmoService.GetGizmosAsync();
    GizmosGridView.DataBind();
}

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

  • La directive Page doit avoir l’attribut Async défini sur « true ».
  • La RegisterAsyncTask méthode est utilisée pour inscrire une tâche asynchrone contenant le code qui s’exécute de manière asynchrone.
  • La nouvelle GetGizmosSvcAsync méthode est marquée avec le mot clé asynchrone , qui indique au compilateur de générer des rappels pour les parties du corps et de créer automatiquement un Task élément retourné.
  • « Async » a été ajouté au nom de méthode asynchrone. L’ajout de « Async » n’est pas obligatoire, mais il s’agit de la convention lors de l’écriture de méthodes asynchrones.
  • Le type de retour de la nouvelle GetGizmosSvcAsync méthode est Task. Le type de retour de Task représente le travail en cours et fournit aux appelants de la méthode un handle à travers lequel attendre la fin de l’opération asynchrone.
  • Le mot clé Await a été appliqué à l’appel du service web.
  • L’API de service web asynchrone a été appelée (GetGizmosAsync).

À l’intérieur du corps de la GetGizmosSvcAsync méthode, GetGizmosAsync une autre méthode asynchrone est appelée. GetGizmosAsync retourne immédiatement une Task<List<Gizmo>> opération qui finira par se terminer lorsque les données sont disponibles. Comme vous ne souhaitez rien faire d’autre tant que vous n’avez pas les données de gizmo, le code attend la tâche (à l’aide du 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 comme rappel sur la tâche et retourne immédiatement. Une fois la tâche attendue terminée, elle appellera ce rappel et reprendra ainsi l’exécution de la méthode à droite où elle s’est arrêtée. Pour plus d’informations sur l’utilisation des mots clés Await et async et de l’espace de noms De tâche , 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 (WebClient webClient = new WebClient())
    {
        return JsonConvert.DeserializeObject<List<Gizmo>>(
            await webClient.DownloadStringTaskAsync(uri)
        );
    }
}

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

  • La signature de méthode a été annotée avec le mot clé async , le type de retour a été modifié Task<List<Gizmo>>et Async a été ajouté au nom de la méthode.
  • La classe HttpClient asynchrone est utilisée au lieu de la classe WebClient synchrone.
  • Le mot clé Await a été appliqué à la méthode asynchrone HttpClientGetAsync .

L’image suivante montre l’affichage gizmo asynchrone.

Capture d’écran de la page de navigateur web Gizmos Async montrant la table des gizmos avec des détails correspondants comme entrés dans les contrôleurs d’API web.

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.

RegisterAsyncTask Notes

Les méthodes connectées RegisterAsyncTask s’exécutent immédiatement après PreRender.

Si vous utilisez directement des événements de page void asynchrones, comme indiqué dans le code suivant :

protected async void Page_Load(object sender, EventArgs e) {
    await ...;
    // do work
}

vous n’avez plus de contrôle total sur le moment où les événements s’exécutent. Par exemple, si un .aspx et un . Les événements de définition Page_Load maître et l’un ou les deux sont asynchrones, l’ordre d’exécution ne peut pas être garanti. Le même ordre indéterminé pour les gestionnaires d’événements (par exemple async void Button_Click ) s’applique.

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

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

protected void Page_Load(object sender, EventArgs e)
{
    Stopwatch stopWatch = new Stopwatch();
    stopWatch.Start();

    var widgetService = new WidgetService();
    var prodService = new ProductService();
    var gizmoService = new GizmoService();

    var pwgVM = new ProdGizWidgetVM(
        widgetService.GetWidgets(),
        prodService.GetProducts(),
        gizmoService.GetGizmos()
       );
    WidgetGridView.DataSource = pwgVM.widgetList;
    WidgetGridView.DataBind();
    ProductGridView.DataSource = pwgVM.prodList;
    ProductGridView.DataBind();
    GizmoGridView.DataSource = pwgVM.gizmoList;
    GizmoGridView.DataBind();

    stopWatch.Stop();
    ElapsedTimeLabel.Text = String.Format("Elapsed time: {0}", 
        stopWatch.Elapsed.Milliseconds / 1000.0);
}

Le code asynchrone PWGasync derrière est illustré ci-dessous.

protected void Page_Load(object sender, EventArgs e)
{
    Stopwatch stopWatch = new Stopwatch();
    stopWatch.Start();
    RegisterAsyncTask(new PageAsyncTask(GetPWGsrvAsync));
    stopWatch.Stop();
    ElapsedTimeLabel.Text = String.Format("Elapsed time: {0}",
        stopWatch.Elapsed.Milliseconds / 1000.0);
}

private async Task GetPWGsrvAsync()
{
    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
       );

    WidgetGridView.DataSource = pwgVM.widgetList;
    WidgetGridView.DataBind();
    ProductGridView.DataSource = pwgVM.prodList;
    ProductGridView.DataBind();
    GizmoGridView.DataSource = pwgVM.gizmoList;
    GizmoGridView.DataBind();           
}

L’image suivante montre l’affichage retourné à partir de la page ASYNCHRONE PWGasync.aspx .

Capture d’écran de la page de navigateur web Widgets, Produits et Gizmos asynchrones montrant les tables Widgets, Products et Gizmos.

Utilisation d’un jeton d’annulation

Les méthodes asynchrones retournées Tasksont annulables, c’est-à-dire qu’elles prennent un paramètre CancelToken lorsqu’une méthode est fournie avec l’attribut AsyncTimeout de la directive Page . Le code suivant montre la page GizmosCancelAsync.aspx avec un délai d’expiration de la seconde.

<%@ Page  Async="true"  AsyncTimeout="1" 
    Language="C#" AutoEventWireup="true" 
    CodeBehind="GizmosCancelAsync.aspx.cs" 
    Inherits="WebAppAsync.GizmosCancelAsync" %>

Le code suivant montre le fichier GizmosCancelAsync.aspx.cs .

protected void Page_Load(object sender, EventArgs e)
{
    RegisterAsyncTask(new PageAsyncTask(GetGizmosSvcCancelAsync));
}

private async Task GetGizmosSvcCancelAsync(CancellationToken cancellationToken)
{
    var gizmoService = new GizmoService();
    var gizmoList = await gizmoService.GetGizmosAsync(cancellationToken);
    GizmosGridView.DataSource = gizmoList;
    GizmosGridView.DataBind();
}
private void Page_Error(object sender, EventArgs e)
{
    Exception exc = Server.GetLastError();

    if (exc is TimeoutException)
    {
        // Pass the error on to the Timeout Error page
        Server.Transfer("TimeoutErrorPage.aspx", true);
    }
}

Dans l’exemple d’application fourni, la sélection du lien GizmosCancelAsync appelle la page GizmosCancelAsync.aspx et illustre l’annulation (en minutant) de l’appel asynchrone. Étant donné que le délai se trouve dans une plage aléatoire, vous devrez peut-être actualiser la page quelques fois pour obtenir le message d’erreur de délai d’expiration.

Configuration du serveur pour les appels de service web haute latence/haute concurrence

Pour bénéficier des avantages d’une application web asynchrone, vous devrez peut-être apporter des modifications à la configuration du serveur par défaut. Gardez à l’esprit ce qui suit lors de la configuration et du test de stress de votre application web asynchrone.

  • Windows 7, Windows Vista, Window 8 et tous les systèmes d’exploitation clients Windows ont un maximum de 10 requêtes 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 à l’aide de la commande suivante :
    %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 HTTP.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 les demandes avec un état 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, puis sélectionnez Paramètres avancés.
      Capture d’écran du Gestionnaire des services Internet Information montrant le menu Paramètres avancés mis en surbrillance avec un rectangle rouge.
    • Dans la boîte de dialogue Paramètres avancés , remplacez la longueur de la file d’attente de 1 000 à 5 000.
      Capture d’écran de la boîte de dialogue Paramètres avancés montrant le champ Longueur de la file d’attente défini sur 1000 et mis en surbrillance avec un rectangle rouge.

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

  • Contrôle de version et multi-ciblage .NET - .NET 4.5 est une mise à niveau sur place vers .NET 4.0

  • Comment définir une application IIS ou AppPool pour utiliser ASP.NET 3.5 plutôt que 2.0

  • Versions et dépendances du .NET Framework

  • Si votre application utilise des services web ou System.NET pour communiquer avec un back-end via HTTP, vous devrez peut-être augmenter l’élément connectionManagement/maxconnection . Pour les applications ASP.NET, cela est limité par la fonctionnalité de configuration automatique à 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. Comme cela est lié à la configuration automatique, 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 dans le Application_Start fichier global.asax . Consultez l’exemple de téléchargement pour un exemple.

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

Contributeurs