Article L'asynchrone : pourquoi et comment ? - Version imprimable +- JeuWeb - Crée ton jeu par navigateur (https://jeuweb.org) +-- Forum : Discussions, Aide, Ressources... (https://jeuweb.org/forumdisplay.php?fid=38) +--- Forum : Programmation, infrastructure (https://jeuweb.org/forumdisplay.php?fid=51) +--- Sujet : Article L'asynchrone : pourquoi et comment ? (/showthread.php?tid=8197) |
L'asynchrone : pourquoi et comment ? - Xenos - 14-09-2020 L'asynchrone : pourquoi et comment ? Le problème des opérations longues et/ou planifiées Lorsqu'on développe une application Web, on rencontre des tâches qui peuvent être longues et/ou qui doivent être planifiées dans le temps. Parmi les tâches longues à effectuer, on trouve notamment :
Parmi les opérations à planifier dans le temps (à instants fixes ou cycliques), on trouve :
Toutes ces tâches peuvent impacter négativement sur les performances de l'application et/ou l'expérience utilisateur. L'enjeu des traitements asynchrones sera alors de limiter voire supprimer ces aspects négatifs. Effectuer des opérations longues Effectuer les opérations longues au sein des requêtes HTTP L'approche la plus évidente est d'effectuer ces opérations à même le code, à l'intérieur du cycle de vie de la requête HTTP, tout comme on ferait une affectation de variable ou une requête SQL. Toutefois, cette approche a de nombreux défauts en terme de performance et d'expérience utilisateur. Impact sur les performances Lorsqu'un navigateur charge une page Web, il se connecte au serveur Web (Apache, par exemple) grâce au protocole HTTP. La connexion est initialisée, le serveur Web exécute le script et renvoie une réponse. Tout cela se passe généralement en moins d'une seconde (le plus souvent autour de 200 millisecondes). Si plusieurs utilisateurs naviguent sur le site, le serveur Web est capable de gérer plusieurs requêtes simultannées (on dit que ces requêtes sont concurrentes). Cependant, le nombre de requêtes HTTP que peut traiter un serveur Web dépend de sa configuration, qui dépend elle-même des performances du serveur : un serveur très puissant pourra traiter plus de requêtes concurrentes qu'un serveur plus modeste. Si le serveur peut gérer 5 requêtes concurrentes et qu'à un même instant, 6 personnes chargent une page, toutes les files seront prises et le sixième visiteur devra attendre qu'une file se libère. Ainsi, on voit qu'avoir des pages longues à charger réduit considérablement les capacités d'un serveur Web. Impact sur l'expérience utilisateur Outre le fait que le serveur soit plus rapidement surchargé quand les pages sont lentes, l'autre conséquence directe pour l'utilisateur, c'est bien d'attendre que les actions s'effectuent ! Ainsi, quand l'utilisateur s'inscrit et qu'un mail de confirmation est envoyé, l'opération prend souvent plus d'une seconde et le navigateur passe autant de temps à charger la page : ce n'est pas très agréable et ça donne déjà une mauvaise image du jeu. Imaginez alors le script qui permet à l'administrateur d'envoyer un email personnalisé à tous ses joueurs… Effectuer les opérations longues en tâche de fond Pour extraire ces opérations des requêtes HTTP, on peut les exécuter en tâche de fond grâce à un daemon installé aux côtés du serveur Web. L'utilisation de Cron est également une possibilité. L'utilisation d'une telle solution implique l'utilisation d'un serveur dédié ou d'une plateforme cloud. Cela peut demander des compétences en administration système. De plus, cela implique certains coûts souvent supérieurs à ceux des hébergements mutualisés. Aux tarifs observés en septembre 2011, comptez 220€ par an environ pour une solution dédiée. Pour implémenter ça en Ruby, j'utilise Delayed Job. Parmi les alternatives : Resque (avec Resque Scheduler pour planifier les tâches), qui ne sera pas couvert ici. Des portages de ces deux solutions existent pour PHP avec DJJob et PHP Resque. Exemples Prenons un jeu de stratégie dans lequel les joueurs rejoignent des parties et s'y affrontent. Une fois le nombre de joueur atteint, chaque joueur peut commencer à déployer ses troupes. Cette phase est limitée dans le temps. Pour implémenter ça simplement, il suffit — au moment où le déploiement commence — d'enregistrer une tâche qui sera exécutée à un instant précis.
Et la classe associée, qui a seulement besoin d'implémenter une méthode perform.
Ensuite, on lance le processus qui traiteront les tâches : le worker. On peut d'ailleurs lancer plusieurs workers si le nombre de tâche en queue est important.
Mise en pratique des différentes approches Pour déclencher des événements qui mènent à la mise à jour de certaines informations, certains développeurs testent des conditions à chaque chargement. Cela ajoute de la complexité au code et le rend plus difficile à tester. Reprenons l'exemple de notre jeu de stratégie ou la durée du tour est limitée dans le temps et prenons les différentes approches. Approche synchrone À chaque chargement de page d'un joueur de la partie, le système teste si le tour du joueur actif n'est pas terminé depuis la dernière exécution de ce test. Voici à quoi pourrait ressembler le code d'une telle page :
Ce n'est pas bien compliqué : on a une page show accessible à l'URL /games/42 (où 42 est l'ID de la partie) dans laquelle on récupère les informations de la partie, puis on teste si l'heure de fin du tour est passée. Si oui, on change de tour. Enfin, on rend la vue HTML de l'action. Si la date n'est pas passée, rien ne se passe et le joueur accède à sa page normalement : même dans ce cas, le test a consommé des ressources inutilement. Ici, le test est très simple, mais il peut être plus complexe (dans le cas d'un calcul au prorata des ressources gagnées depuis le dernier passage du joueur, par exemple). Si le joueur actif a changé, le système effectue les opérations nécessaires pour passer la main au joueur suivant. Ce traitement peut être long et le joueur a alors l'impression que sa page est longue à charger. C'est désagréable pour lui. L'autre défaut de cette approche est que les joueurs ne remarquent les changements que quand ils actualisent la page. Approche légèrement asynchrone Ici, notre développeur utilise des requêtes asynchrones du client vers le serveur : ce qu'on appelle communément Ajax. Régulièrement, les joueurs demanderont discrètement au serveur si l'état de la partie a changé. Attention : on devra continuer de faire les tests.
Les requêtes cycliques :
Ici, on ajoute une nouvelle action check_turn qui va elle aussi déclencher le filtre (et changer de tour si besoin). Cette action renverra un peu de JSON pour indiquer si le tour a changé ou non et le nouveau joueur actif s'il y a eu changement. Le navigateur testera la réponse et apportera les changements nécessaires à l'interface pour notifier ce changement. Au bout du compte, le changement de tour sera le plus souvent (mais pas toujours) effectué au sein d'une requête Ajax, les joueurs auront donc moins souvent cette sensation de lenteur du chargement. Le problème, c'est que le serveur va régulièrement recevoir de nombreuses requêtes et que certaines risquent d'être longues à répondre (risquant donc de déclencher le mécanisme de file d'attente). Le problème d'expérience utilisateur est partiellement résolu mais ajoute le problème de la charge serveur. Approche fortement asynchrone Le développeur utilise des traitements asynchrones des deux côtés : les gros traitements s'effectuent en tâche de fond et les informations sont envoyées aux joueurs grâce au push. Ici, l'approche est plus efficace : puisqu'on sait quand arrivera le tour suivant, on programme un événement. Il n'y aura pas de pertes :
Côté client, on écoute les messages envoyés par le serveur :
Conclusion Le mode de pensée asynchrone est très inhabituel pour un développeur Web débutant mais beaucoup plus efficace que le modèle synchrone. Il permet de tirer profit des ressources de sa machine en utilisant la parallélisation des processus et en évitant le gâchis de ressources. De plus, c'est un fonctionnement plus agréable pour vos utilisateurs. La moralité est qu'en investissant un peu dans une infrastructure dédiée (comptez 220€ par an, en septembre 2011), on peut décupler les possibilités et la qualité de nos jeux. |