Javascript utilise un modèle de gestion de la concurrence mono-thread, cet article présente quelques réflexions et exemples pour comprendre les bases du module de concurrence de JS.
Nous allons étudier cela grâce à deux démonstrations: une première boucle séquentielle et synchrone puis une seconde avec du parallélisme et de l'asynchronicité.
Brièvement : en utilisant un langage traditionnel comme C ou Java, le programme fait parfois des choses qui prennent un certain temps pour que le CPU calcule.
Pendant ce temps, on ne peut pas exécuter d'autre code que les instructions actuelles. Le programme peut être gelé et l'interface utilisateur risque de ne pas répondre du tout le temps que cette opération se termine.
C'est une comportement tout à fait « normal » et l'ont doit malheureusement faire avec…
➡️ Comment empêcher les blocages d'interface utilisateur ?
Pour résoudre ce problème, on peut utiliser un thread, qui permet de déléguer l'exécution d'un code dans un sous-processus. Ceci conduit à des actions non-bloquantes et à l'exécution de code parallèle grâce au multithreading.
C'est la base du parallélisme de code et de la gestion de la concurrence dans les langages standards/anciens.
La gestion de la concurrence est un chapitre compliqué de la programmation, plus particulièrement pour les débutants. Je ne creuserais donc pas plus que cela le sujet ici. Ce que nous avons vu plus haut est amplement suffisant pour comprendre ce qui suit, étant donné que Javascript n'est pas un language multi-threadé.
Du coup:
❓ Comment traite-t-on les problèmes de concurrence avec Javascript ?
Comme nous avons pu le voir ci-dessus, JavaScript est un langage mono-threadé, cela est vrai pour n'importe quelle implémentation de moteur JS comme V8 de Chrome, JavaScriptCore pour Safari d'Apple ou Hermes de Facebook pour React native.
❓ Qu'est-ce que cela signifie ?
Cela signifie que JavaScript ne pourra exécuter qu'une seule tâche à la fois et que chaque tâche sera mise en file d'attente pour être traitée.
Mais comme nous l'avons vu plus haut, nous aurons parfois besoin de faire plusieurs choses à la fois ou bien nous aurons des instructions qui peuvent prendre plusieurs secondes à traiter, ce qui conduit à des blocage à cause du thread blocking.
Donc, la seule façon d'y faire face est de déléguer l'exécution du code à un gestionnaire externe, de continuer l'exécution du script et d'exécuter un code de rappel une fois que l'exécution a été traitée.
Et c'est ce qui est implémenté par le moteur JSs dans quelque chose qui se nomme l' « event loop ».
JavaScript peut déléguer des comportements asynchrones au système d'exploitation en utilisant certaines fonctions « natives » fournies par le noyau.
La plupart du temps ce sont des opération IO, mais cela peut être autre chose.
Une fois ces actions appelées, le système prend les ordres, exécute toutes les instructions dans son coin dans des sous-processus....
Ensuite le système va rapeller le programme en executant une callback: cela fait parti du fameux cycle de la boucle d'évènement Javascript.
Si vous n'avez jamais entendu parler de cela, jetez-donc un oeil ici: https://developer.mozilla.org/fr/docs/Web/JavaScript/Concurrence_et_boucle_des_événements
💣 Brièvement: La boucle d'événement est un élément du moteur JS qui tourne en boucle indéfiniment pendant que le programme est en cours d'exécution, cette boucle exécutera des instructions dans une file d'attente dans un ordre spécifique.
Ces instructions sont fournies par des instructions de rappel.
Les instructions de rappel fournissent du code qui doit être exécuté une fois que le système a terminé l'exécution asynchrone du code délégué, comme par exemple lire un fichier sur le système.
On parle d'instructions de rappel mais en réalité il y en a deux types:
✋🏻⚠️ Je ne développerai pas ici ce qu'est une promesse (Promise), vous pouvez consulter ce guide très bien écrit à ce sujet : https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Promise
Dans l'exemple suivant, nous allons itérer à travers un tableau de thématiques pour démontrer la différence entre une boucle bloquante et une boucle non-bloquante utilisant des comportements asynchrones.
Pour chaque thématique nous avons besoin de :
✋🏻 Tous les exemples de codes suivants sont composés de code bouchon qui ne font que retarder le retour de la fonction en utilisant une Promesse Javascript. C'est une façon de simuler une tâches déléguée au système.
Afin d'utiliser le snippet suivant, vous devrez créer un fichier index.js, copier les snippets de code donnés et exécuter un npm init -y && npm install chalk afin d'installer cette petite librairie utilitaire qui permet d'utiliser de la couleur dans le texte de la console.
Il suffit ensuite de lancer le script en tapant node ./index.js
On commence par l'approche « synchrone », celle-ci bloquera l'exécution dans la boucle en utilisant une fonction asynchrone avec le mot-clé await tant que la fonction appelée n'aura pas retourné de valeur.
💡 Info : En fait, ce n'est pas vraiment synchrone comme on le pense. L'utilisation de await ne fera que s'assurer que le code est exécuté séquentiellement au sein de la fonction. Chaque fois que l'on attends, on laisses d'autres instructions s'exécuter ailleurs et plus tard le programme continuera où nous attendions.
Ceci conduit aux postulats suivants :
Bien que cela puisse parfois être intéressant pour certains cas d'utilisation pour garantir le séquençage lorsque les contenus ont des effets de bords les uns sur les autres, dans le cas d'utilisation suivant, ce n'est pas la meilleure façon d'atteindre notre but.
En exécutant le script suivant dans un programme NodeJS, vous pouvez voir que le temps total d'exécution du script est assez élevé par rapport, mais comme nous le séquençons, le temps d'exécution augmente de manière linéaire.
Comme chacune de nos thématiques n'ont aucun rapport entre elles en termes de dépendance et d'effets de bord, notre fonction de rappel loopOccurence est presque une fonction pure, car elle n'a pas d'effet de bords sur notre application. (pour être une vraie fonction pure, elle ne devrait pas avoir d'effets de bord du tout).
Et pour cette raison, je vous invite à voir ce qui se passe si vous supprimez le mot-clé await dans le snippet précédent comme décrit ci-dessous.
💡 Les mots clés async / await sont des outils pour contrôler le flux d'exécution d'une fonction. En supprimant le mot-clé await, on demande à l'interpréteur de ne pas attendre que la fonction réponde avant de passer à la prochaine itération de la boucle.
Cette approche tire parti du parallélisme puisque le moteur JS envoie les trois requêtes en même temps.
💡 Info : Ah oui, j'oubliais, vous n'utilisez pas await à l'intérieur de la boucle mais cela ne veut pas dire qu'il ne faut pas l'utiliser du tout, en fait vous devrez gérer les erreurs et vous assurer que tout se passe bien. Nous verrons cela dans la section suivante.
Les performances et la réactivité de l'application sont grandement améliorées et nous pouvons toujours garder notre séquençage à l'intérieur de chaque boucle de thématiques en attendant d'abord l'état de retour d'appel de l'API et ensuite créer le fichier.
Si l'on considère le temps d'exécution, c'est bien mieux que notre première approche.
Pourquoi ? Parce que JS délègue l'exécution de manière parallèle, de sorte que le script peut accéder plus rapidement aux autres instructions (itérations de boucle dans notre cas), ce qui se traduit par de meilleures performances.
Nous aurions également pu remplacer notre boucle for...of par une fonction de programmation fonctionnelle nommée forEach. Le résultat serait exactement le même que celui que vous voyez ici.
💡 Voulez-vous en savoir plus sur la fonction forEach ? Cliquez ici !
En fait, il me manquait quelque chose de vraiment important : m'assurer que tout fonctionne bien et gérer les erreurs.
Vous pouvez améliorer l'extrait ci-dessus avec ce qui suit en utilisant une combinaison de la fonction map de la programmation fonctionnelle et de la fonction Promise.all.
La première volonté permet de mapper la promesse qui sera transmise à la seconde. La seconde recevra un tableau de promesses et fera un « échec rapide » si l'une des promesses du tableau donné échoue. Il s'agit de notre scénario de gestion des erreurs.