ASP.NET 2.0 permet nativement l'exécution asynchrone de certains traitements dans les pages, mais cette fonctionnalité est rarement connu, ou alors considérée comme « peu utile » alors qu'elle est dans certains cas très utile et permet parfois d'éviter des problèmes de performances.

Déjà, pourquoi vouloir de l'asynchrone dans une page web ?

En effet ASP.NET se sert de tous les threads disponibles de son pool de thread pour traiter les requêtes entrantes. Chaque requête est déjà traitée par un thread différent, il y donc déjà une forme d'"asynchrone" (heureusement !). Pour bien voir l'intérêt considérons un exemple : Une page faisant une "grosse" requête vers... une ressource quelconque (SGBD, webservice, accès au réseau, ou un simple "tracert"). Cette page va mettre quelques milisecondes à s'exécuter en elle-même, et par exemple 2 secondes à attendre la réponse de son traitement. Cette page monopolise ainsi un thread pendant un peu plus de 2s alors que le thread est "inactif" pendant la majorité du temps !

D'où l'intérêt de libérer le thread en attendant la réponse du traitement et de laisser ainsi ASP.NET "donner" ce thread à d'autres pages plus légères (=plus rapides).

Comment exécuter une page de manière asynchrone ?

  1. Ajouter une directive Async="true" dans mapage.aspx :

<% @ Page Language="C#" ... Async="true" ... %>

Cette directive indique simplement au compilateur de faire implémenter par la page IHttpAsyncHandler au lieu de IHttpHandler. Cette interface permet à la page d'exposer des fonctionnalités de traitements asynchrones.

  1. Déclarer, par exemple dans le Page_Load (mais au plus tard dans le Pre_Render !), sa nouvelle « Tâche » :

protected void Page_Load(object sender, EventArgs e)

{

// création d'une nouvelle tâche

// en lui passant les 3 handlers possible

PageAsyncTask task = new PageAsyncTask(new BeginEventHandler(BeginAsyncTask),

new EndEventHandler(EndAsyncTask), new EndEventHandler(TimeoutAsyncTask), null);

// enregistrement de la tâche

Page.RegisterAsyncTask(task);

}

 

IAsyncResult BeginAsyncTask(object sender, EventArgs e,

AsyncCallback cb, object state)

{

// operation longue à réaliser (on va dire qu'il y a 10 000 000 d'utilisateurs en base :p)

_maConnection = new SqlConnection(

@"Data Source=.\SQLExpress;Integrated Security=true;Async=true;AttachDbFilename=C:\mydb.mdf;User Instance=true");

_maConnection.Open();

_maCommand = new SqlCommand(

"SELECT * FROM Users", _connection);

return _command.BeginExecuteReader(cb, state);

 

}

 

void EndAsyncTask(IAsyncResult ar)

{

// affichage des résultats :

_monRepeater.DataSource = _maCommand.EndExecuteReader(ar);

_monRepeater.DataBind();

}

 

void TimeoutAsyncTask(IAsyncResult ar)

{

// timeout !

_monLabel.Text = "Erreur !!";

}

Attention à bien ajouter « Async=true » à la ConnectionString. Ce nouveau paramètre propre à ADO.NET 2 permet de spécifier qu'il s'agit d'un appel asynchrone et permet dans l'exemple d'utiliser les méthodes asynchrones BeginExecuteReader et EndExecuteReader.

On remarquera aussi la présence d'une fonction se déclenchant lors du TimeOut de la tâche. Ce TimeOut (en secondes) peut être spécifié par le code ou dans la directive Page :

<% @ Page Language="C#" Async="true" AsyncTimeout="20" ... %>

Ou dans le web.config :

< system.web >

< pages asyncTimeout ="20" />

</ system.web >

A noter cependant qu'on ne peut pas le spécifier pour une tâche précise, mais seulement pour toutes les tâches d'une page et/ou de tout un site, et que sa valeur par défaut est de… 45 secondes !

Un rapide résumé de ce qu'il se passe exactement lors de l'exécution de la requête :

  1. Un premier thread est donné à la requête. La page est traitée en entier jusqu'au Page_PreRenderComplete (juste avant).
  2. La page liste tous les tâches et les laisse les une après les autres envoyer leur requête asynchrone, puis elle rend le thread au pool ASP.NET.
  3. La/les requête(s) sont en train d'être traités. Ici aucun thread du pool d'ASP.NET n'est consommé !
  4. Quand ces requêtes « reviennent », ASP.NET redonne un thread à la page, qui exécute les fonctions de retour ou de timeout, puis fini l'exécution normale de la page (le Page_PreRenderComplete et la suite).

Enfin, on peut forcer ASP.NET à effectuer toute cette mécanique avant le Page_PreRenderComplete en appelant explicitement la méthode Page.ExecuteRegisteredAsyncTasks.

Outre le simple plaisir de complexifier le code :p, ça peut aussi éviter certains phénomènes d' « encrassement » du pool de thread (certains threads sont trop long à terminer et empêches des requêtes pourtant très rapides à s'exécuter d'obtenir un thread ; ce qui fini en un magnifique « Server Unavailable »pour certaines requêtes !). C'est aussi une possible voie à laquelle il faut s'intéresser lors de problèmes de montée en charge, qui s'attaque peut être plus au vrai problème que de « tout simplement » augmenter la taille du pool de thread ou de passer carrément à un serveur plus puissant !

Comment ça marche ?

 

Le gestionnaire de PageAsyncTask de la Page s'appuie sur la méthode Page.AddOnPreRenderCompleteAsync (que l'on peut d'ailleurs utiliser directement sans passer par le RegisterAsyncTasks, mais on perd certains fonctionnalités utiles, cf. liens plus bas).

Comme cette fonctionnalité est fournie par une interface qui hérite elle-même de IHttpHandler, on peut appliquer ce traitement asynchrone à bien autre chose qu'une page, à n'importe quel Handler (et sur un principe semblable également à n'importe quel Module, cf. liens plus bas). Dans le cas du Handler, il « suffit » de rajouter Begin et End devant le ProcessRequest :

public class MonHttpAnsyncHandler : IHttpAsyncHandler

{

public void ProcessRequest(HttpContext context)

{

// n'est jamais appelé ! (mais necessaire...)

}

 

public bool IsReusable { get { return false; } }

 

public IAsyncResult BeginProcessRequest(HttpContext context,

AsyncCallback callBack, object state)

{

// début du traitement

}

 

 

public void EndProcessRequest(IAsyncResult result)

{

// fin du traitement

}

}

Pour aller plus loin

 

Un bon exemple d'implémentation de handler asynchrone est évidemment la classe System.Web.UI.Page elle-même, bien qu'il ne soit pas « tout simple », cf. les méthodes Page.AsyncPageBeginProcessRequest et Page.RegisterAsyncTasks (pour ceux qui aiment faire mumuse avec Reflector… moi je peux plus m'en empêcher c'est devenu une manie :D)

Sinon, pour ceux qui voudraient aller plus loin dans les PageAsyncTasks ou se plonger dans les Handlers/Modules asynchrones, un peu de lecture J :