ASP.net et l’asynchronisme
Par Patrice Lamarche, posté le 03/09/2009
Profil : Développeur | Niveau : Intermédiaire (200)
Introduction
L'asynchronisme est une des nouveautés majeures apportées par ASP.net 2.0 et pourtant l'une des moins utilisées. Elle est pourtant fondamentale si vous souhaitez avoir une application ASP.net qui supporte de fortes montées en charge sans pour autant nécessiter une architecture physique très importante.
Comment fonctionnent les threads d'ASP.net
Comme pour toutes les applications serveur, vous devez éviter de gérer tout ce qui concerne la gestion des threads. C'est le serveur d'applications (dans notre IIS) qui a la responsabilité de le faire.
En ce qui concerne ASP.net un pool de thread est créé par la CLR pour chaque worker process. Les threads de ce pool sont divisés en deux catégories :
· Les « Worker threads » : threads qui exécutant chaque requête. Ce sont les threads les plus importants, il est nécessaire d'avoir des threads de libre afin de pouvoir exécuter les différentes requêtes arrivant sur votre application web.
· Les threads I/O : Ces threads sont dédiés à toutes les opérations d'entrée sorties. Les applications web étant dans la très grande majorité dirigée par les données, ce sont les threads qui prennent généralement le plus de temps car ils exécutent les tâches les plus coûteuses telles que l'accès à des services web, à des bases de données, etc.
Il est important de distinguer ces deux catégories parce qu'ASP.net limite le nombre de threads alloués à ces deux catégories à 25 threads chacun. Cette configuration par défaut peut être augmentée afin d'atteindre les 100 threads dans chaque pool.
Maintenant que nous distinguons bien les deux types de threads gérés par ASP.net, et avec les quelques informations déjà distillées nous pouvons d'ores et déjà détecter où se trouvent le goulot d'étranglement, quelle partie de l'application web et le plus susceptible d'avoir un impact important sur les performances, la montée en charge. Les threads I/O sont en effet les plus souvent coupables des problèmes de montée en charge que vous êtes amenés à rencontrer lors de l'exécution d'applications ASP.net.
Nous avons en effet indiqué que les Worker Threads étaient ceux qui prenaient en charge les différentes requêtes effectuées sur votre application, et ceux-ci attendent l'exécution des threads I/O afin de pouvoir rendre la page qu'ils doivent exécutées. Si la charge est trop importante, et si les opérations I/O prennent trop de temps, vous n'aurez plus de Worker Threads de disponibles afin de répondre aux nouvelles requêtes. Dans ce cas, ASP.net répondra avec la fameuse erreur 503 « Server unavailable ».
Le problème est donc simple, étant donné que les worker threads attendent l'exécution des opérations I/O c'est-à-dire sont synchrones avec ceux-ci, l'application peut ne pas tenir correctement une forte montée en charge.
La solution va donc consister à casser cette forte dépendance en rendant les worker threads asynchrone avec les threads I/O.
Les pages asynchrones
Les pages asynchrones permettent de résoudre ce problème en découpant l'exécution d'une page ASP.net en deux parties.
Le cycle de vie de la page est quasiment le même que pour des pages ASP.net synchrones « classiques » excepté qu'entre l'évènement PreRender et PreRenderComplete, ASP.net va « rendre la main » en attendant que les opérations d'entrée/sortie soient exécutées. Concrètement, le worker thread associé va être indiqué comme étant libre et comme étant capable de traiter une autre requête en attendant le résultat de l'opération d'entrée/sortie.
Nous allons voir les deux méthodes permettant d'implémenter l'asynchronisme
AddOnPreRenderCompleteAsync
Cette méthode reprend et s'intègre très bien avec l'un des mécanismes d'asynchronisme présent dans le framework .net : le pattern Begin/End.
Afin de l'implémenter vous devez modifier la directive Page de chaque page que vous souhaitez rendre asynchrone en ajoutant l'attribut Async et définissez la valeur de cet attribut à True.
Ensuite, dans l'évènement Load de votre page, enregistrer l'opération asynchrone que vous souhaitez exécuter. Pour cela appelez, la méthode AddOnPreRenderCompleteAsync, en précisant les délégués vers vos méthodes de début de traitement (la méthode Begin qui renvoie IAsyncResult et la méthode End).
Ensuite, exécutez le code asynchrone que vous souhaitez dans la méthode Begin. Récupérez le résultat dans la méthode End, puis effectuez votre traitement final avec votre résultat dans l'évènement PreRenderComplete évoqué ci-dessus.
1: public partial class _Default : System.Web.UI.Page
2:
3: {
4:
5: protected void Page_Load(object sender, EventArgs e)
6:
7: {
8:
9: if(!IsPostBack)
10:
11: {
12:
13: AddOnPreRenderCompleteAsync(BeginReadFromDB, EndReadFromDB);
14:
15: }
16:
17: }
18:
19: private SqlDataReader m_reader;
20:
21: private SqlConnection m_connection;
22:
23: private SqlCommand m_command;
24:
25: IAsyncResult BeginReadFromDB(object sender, EventArgs e, AsyncCallback cb, object state)
26:
27: {
28:
29: m_connection = new SqlConnection(@"Data Source=.\SQLEXPRESS;Asynchronous Processing=true;Initial Catalog=TestDatabase;Integrated Security=True");
30:
31: m_connection.Open();
32:
33: m_command = m_connection.CreateCommand();
34:
35: m_command.CommandType = CommandType.Text;
36:
37: m_command.CommandText = "select * from Employees";
38:
39: return m_command.BeginExecuteReader(cb, state);
40:
41: }
42:
43: void EndReadFromDB(IAsyncResult ar)
44:
45: {
46:
47: m_reader = m_command.EndExecuteReader(ar);
48:
49: }
50:
51: protected void Page_PreRenderComplete(object sender, EventArgs e)
52:
53: {
54:
55: GridView1.DataSource = m_reader;
56:
57: GridView1.DataBind();
58:
59: m_connection.Dispose();
60:
61: }
Exécution de plusieurs tâches asynchrones
Il est tout à fait possible d'exécuter plusieurs tâches en asynchrones durant le chargement d'une page. Si vous souhaitez par exemple accéder à une base de données et à un service web et effectuer ces traitements de manière asynchrone, il suffit d'appeler plusieurs fois la méthode AddOnPreRenderCompleteAsync. A noter que l'exécution de ces différentes tâches sera réalisée de manière séquentielle et non en parrallèle.
RegisterAsyncTask
La deuxième manière d'implémenter l'asynchronisme au sein de vos pages ASP.net est d'utiliser la méthode RegisterAsyncTask.
Au lieu de directement appeler cette méthode, vous devez créer une PageAsyncTask afin de l'initialiser d'une manière très similaire à notre précédente technique. La seule différence est la possibilité de définir un TimeOut.
1: protected void Page_Load(object sender, EventArgs e)
2:
3: {
4:
5: if(!IsPostBack)
6:
7: {
8:
9: var task = new PageAsyncTask(BeginReadFromDB, EndReadFromDB, TimeOut, null);
10:
11: RegisterAsyncTask(task);
12:
13: }
14:
15: }
16:
17: private SqlDataReader m_reader;
18:
19: private SqlConnection m_connection;
20:
21: private SqlCommand m_command;
22:
23: IAsyncResult BeginReadFromDB(object sender, EventArgs e, AsyncCallback cb, object state)
24:
25: {
26:
27: m_connection = new SqlConnection(@"Data Source=.\SQLEXPRESS;Asynchronous Processing=true;Initial Catalog=TestDatabase;Integrated Security=True");
28:
29: m_connection.Open();
30:
31: m_command = m_connection.CreateCommand();
32:
33: m_command.CommandType = CommandType.Text;
34:
35: m_command.CommandText = "select * from Employees";
36:
37: return m_command.BeginExecuteReader(cb, state);
38:
39: }
40:
41: void EndReadFromDB(IAsyncResult ar)
42:
43: {
44:
45: m_reader = m_command.EndExecuteReader(ar);
46:
47: }
48:
49: void TimeOut(IAsyncResult ar)
50:
51: {
52:
53: Literal1.Text = "Accès aux données trop long";
54:
55: }
56:
57: protected void Page_PreRenderComplete(object sender, EventArgs e)
58:
59: {
60:
61: GridView1.DataSource = m_reader;
62:
63: GridView1.DataBind();
64:
65: m_connection.Dispose();
66:
67: }
L'autre différence importante est la possibilité d'éxécuter différents traitements asynchrones non plus uniquement séquentiellement comme précédemment mais de manière parrallèle. C'est une différence fondamentale puisque vos utilisateurs vont ainsi voir une nette différence de performance.
Pour ce faire, il suffit d'utiliser la deuxième surcharge du constructeur de PageAsyncTask afin d'indiquer si la tâche doit être effectuée en parallèle ou non :
1: protected void Page_Load(object sender, EventArgs e)
2:
3: {
4:
5: if(!IsPostBack)
6:
7: {
8:
9: var task = new PageAsyncTask(BeginReadFromDB, EndReadFromDB, TimeOut, null,true);
10:
11: var task2 = new PageAsyncTask(BeginReadFromWS, EndReadFromWS, TimeOut, null, true);
12:
13: RegisterAsyncTask(task);
14:
15: RegisterAsyncTask(task2);
16:
17: }
18:
19: }
Conclusion
Nous avons vu les deux manières d'implémenter des pages asynchrones en ASP.net et en quoi l'utilisation de ces mécanismes permet d'avoir une bien meilleure montée en charge pour vos applications web. Bien entendu le but n'est pas de rendre toutes vos pages asynchrones ce qui représenterait une surcharge de travail plus ou moins importante, la première chose à faire lorsque vous souhaitez utiliser cette technique et de faire des tests de montée en charge afin de déceler quelles sont les pages qui pénalisent le plus votre application. L'implémentation technique est ensuite assez simple.
Bon courage !



