Published on

Javascript, single-threaded ou multi-threaded ?

 23 mins
Authors
  • avatar
    Name
    Léo Delpon
    Twitter

Javascript, multi-thread ou single-thread?

Pourquoi cette question existentielle ?

En me baladant sur LinkedIn, je suis tombé sur un article très sympathique. Il décrit globalement comment fonctionne javascript en termes de synchrone / asynchrone. Néanmoins, ce n’est pas le contenu qui m’a interpellé mais plutôt un commentaire qui était dans ce poste. Il disait “Javascript dans le navigateur est bel et bien multithread”.

Après avoir repris mes esprits, je me rend compte, que le commentateur continue de confirmer que “javascript est multithreaded”.

GIF one piece

Je me dis, peut-être que c’est vrai et que j’ai dû mal comprendre comment ça fonctionnait (ce qui constitue une reflexion très (peut-être trop) récurrente chez un développeur) et qu’il fallait que je me renseigne un peu plus. Je commence à rechercher comme un développeur et je pose la question à mon cher navigateur.

Untitled

Visiblement, même google me dit que c’est single-threaded . Je reprend confiance en mes compétences et commence à réfléchir sur comment expliquer le principe single-threaded de javascript. Pourtant, pendant cette phase de reflexion, je me souviens qu’il était possible de réaliser avec javascript du multithread. Comment puis-je avoir cette pensée alors que moi-même je considère javascript comme single-threaded ?

Retour aux bases, c’est quoi un processus ?

Afin d’expliquer tout ceci, prenons comme exemple le programme d’un navigateur comme Google Chrome . Ce programme lorsqu’il est exécuté devient alors un processus. En effet, on appelle un processus l’instance d’un programme en cours d’exécution. Il va donc comprendre :

  • Un espace d’adressage propre à lui-même.
  • Des ressources qui auront été allouées par le système d’exploitation. Dans ces ressources on pourra par exemple retrouver de la mémoire ou encore des variables d’environnements.

Il faut savoir que de base, chaque processus fonctionne indépendamment des autres processus en cours d’exécution. Cependant, Il se peut qu’ils puissent communiquer et intéragir ensemble. On appelle ceci la communication inter-processus ou IPC.

ImaginezGoogle Chrome comme un chef d'orchestre dirigeant une grande symphonie de musiciens. Les musiciens sont les onglets, les extensions et les plug-ins, chacun jouant leur propre partition et ayant leurs propres pupitres (c'est-à-dire les processus). Le chef d'orchestre Chrome veille à ce que chacun reste dans son propre espace, afin qu'ils ne se mélangent pas les partitions et ne causent pas de fausses notes.

test.drawio (6).png

Alors que Chrome dirige cette symphonie de musiciens multitâches, il veille à ce qu'ils restent concentrés et ne soient pas perturbés par les autres membres de l'orchestre. Si un musicien commet une erreur ou casse une corde, Chrome peut l'isoler sans affecter les autres membres de l'orchestre, et la symphonie continue sans heurts.

En fin de compte, Chrome est le maestro qui s'assure que cette grande symphonie de processus et de threads travaille ensemble pour vous offrir la plus belle des mélodies, c'est-à-dire une expérience de navigation rapide, stable et sécurisée. Et la meilleure partie ? Vous êtes au premier rang pour profiter de ce concert virtuose chaque fois que vous utilisez Google Chrome !

Et donc, c’est quoi un thread dans la technique ?

Reprenons l'analogie du chef d'orchestre, chaque musicien (processus) a plusieurs mains (les threads). Ces mains magiques sont capables de jouer plusieurs instruments en même temps et de travailler ensemble pour que la mélodie soit harmonieuse et fluide. Par exemple, l'une des mains peut jouer du violon (rendu de la page Web), une autre main peut jouer du piano (gestion des événements utilisateur), et une autre main peut jouer de la guitare (exécution des scripts JavaScript). A vrai dire, ce n'est pas réellement le navigateur qui gère ça mais plutôt le moteur javascript du navigatuer.

test.drawio (7).png

Un "thread", en revanche, est une unité plus légère d'exécution au sein d'un processus. Il partage l'espace d'adressage et les ressources du processus auquel il appartient. Plusieurs threads peuvent s'exécuter simultanément et travailler en parallèle sur des tâches différentes, ce qui permet d'améliorer les performances et l'efficacité d'un programme, en particulier sur les systèmes multiprocesseurs et multicœurs.

Les processus et les threads sont complémentaires car ils permettent aux programmeurs de tirer parti de l'architecture du système et des ressources pour créer des applications plus efficaces et performantes. Les processus sont utiles pour isoler les tâches et garantir la stabilité du système, tandis que les threads permettent d'améliorer la parallélisation et l'utilisation des ressources au sein d'un processus.

Du coup, comment fonctionne javascript ?

Je vais éviter de m'attarder sur ce point, car l'article que j'ai cité aborde principalement le sujet du synchrone / asynchrone. Toutefois, l'auteur y évoque également le fonctionnement de JavaScript. Malgré cela, je vais quand même aborder ce sujet car c'est mon article et j'ai envie d'en parler !

Vous savez surement tous que javascript est un langage client-side ce qui signifie qu’il s’exécute sur le navigateur du client.

<!DOCTYPE html>
<html lang="fr">
	<head>
		<meta charset="utf-8">
		<title>Fonctionnement de javascript dans un navigateur</title>
	</head>
	<body>
		<h1>This how javascript works</h1>
		<script>
			   console.log("Je suis devenu expert javascript en 6 mois mdrrr");
				 alert("Comment allez-vous ?");
		</script>
	</body>
</html>

Dans le programme ci-dessus, nous avons une balise script qui va dans un premier temps afficher dans la console le message "Je suis devenu expert javascript en 6 mois mdrrr" puis va afficher une alerte avec comme contenu "Comment allez-vous ?" .

Untitled

En observant le programme exécuté sur notre navigateur, nous constatons que cela est rendu possible grâce au moteur javascript intégré dans le navigateur. Ce moteur interprète et exécute le code Javascript, ligne par ligne, permettant ainsi son exécution sur notre navigateur. Dans notre cas, l'utilisation de Google Chrome signifie que notre programme est exécuté grâce au moteur V8.

Voici comment fonctionne simplement un moteur javascript (si vous êtes intéressé pour que j’écrive un article complet sur les moteurs javascript, n’hésitez pas à m’envoyer un message sur LinkedIn !) :

test.drawio (9).png

Lorsque nous exécutons un code JavaScript dans un navigateur web, le moteur du navigateur passe par plusieurs étapes pour interpréter et exécuter le code. Par exemple, si nous avons du code qui utilise la fonction console.log() pour afficher un message dans la console du navigateur, le moteur JS suit les étapes suivantes:

  • Étape 1 : L'analyseur syntaxique reçoit le code JavaScript et vérifie qu'il ne contient pas d'erreurs de syntaxe. Dans notre exemple, il vérifie que la fonction console.log() est correctement écrite et que tous les arguments nécessaires sont fournis.
  • Étape 2 : Une fois que l'analyseur syntaxique a vérifié le code, il crée une structure de données appelée AST (Abstract Syntax Tree). Cette structure permet au moteur JS de comprendre comment le code doit être exécuté.
  • Étape 3 : Le moteur JS interprète l'AST et exécute le code. Dans notre exemple, la fonction console.log() affiche le message fourni dans la console du navigateur.

De même, si nous utilisons la fonction alert() pour afficher une boîte de dialogue avec un message à l'utilisateur, le moteur JS suit les mêmes étapes que pour console.log(). L'analyseur syntaxique vérifie que la fonction alert() est correctement écrite et que tous les arguments nécessaires sont fournis. Ensuite, l'AST est créée, et enfin la fonction alert() est exécutée pour afficher la boîte de dialogue avec le message spécifié.

En supplément au moteur javascript, on note deux éléments qui sont une partie importante du moteur :

  • La Call stack permet de garder la trace des appels des fonctions. Le fonctionnement est le même que le principe LIFO (last In First Out).
  • La Heap memory c’est une zone mémoire alloué pour chaque programme. On peut allouer dans cette zone de la mémoire de façon dynamique, contrairement à la mémoire allouée dans la call stack.

On considère ainsi que javascript est single threaded car il ne possède d’une seule call stack pour exécuter un programme.

Mais alors, comment ça se fait qu’on peut faire quand même du multithreading avec javascript ?

Comme vous le savez, Javascript est single-threaded par défaut. Par conséquent, si nous désirons effectuer un processus qui prend beaucoup de temps, l’application (le navigateur web) risque de ne plus répondre. Mais il existe des outils plutôt cool qui sont mis à disposition pour pallier à ce problème !

Les workers

Les Web Workers introduisent le multithreading dans les applications web JavaScript, permettant ainsi à différentes parties du code de s'exécuter simultanément sans se bloquer mutuellement. Ils peuvent communiquer en utilisant des techniques de transmission de messages. Contrairement au code asynchrone, où chaque rappel (callback) doit attendre son tour pour s'exécuter dans le thread principal, les Web Workers permettent à plusieurs modules d'être exécutés en parallèle, sans interférer les uns avec les autres. Les Web Workers sont exécutés dans un environnement de thread séparé, ce qui signifie qu'ils ont leur propre contexte d'exécution et n'ont pas accès aux variables ou aux fonctions du thread principal. Les communications entre le thread principal et les Web Workers se font par le biais d'un système de messagerie basé sur des événements.

Il existe deux types de Web Workers :

  • Les "Dedicated Workers" sont des travailleurs dédiés qui ne peuvent être utilisés que par un seul script. Ils sont créés en utilisant la classe Worker et peuvent être utilisés pour effectuer des tâches intensives en calculs ou en réseau.
  • Les "Shared Workers" sont des travailleurs partagés qui peuvent être utilisés par plusieurs scripts sur la même page Web ou dans différentes pages Web du même domaine. Ils sont créés en utilisant la classe SharedWorker et peuvent être utilisés pour partager des données et des ressources entre plusieurs scripts.

Voilà comment un Dedicated Worker fonctionne par exemple:

test.drawio (10).png

Pourquoi sont-ils utilisés ?

Les Web Workers sont une fonctionnalité du navigateur ou de Node.js qui permettent d'exécuter des tâches intensives en calculs, en réseau ou en lecture/écriture de fichiers, qui peuvent prendre du temps et ralentir le fonctionnement de l'application. En les utilisant, le thread principal de l'application peut être libéré pour continuer à répondre aux interactions de l'utilisateur et maintenir une expérience fluide.

Les Web Workers sont souvent utilisés pour:

  • Exécuter des tâches telles que des calculs complexes, comme la génération d'images, le traitement de données ou la simulation physique.
  • Ils peuvent également être utilisés pour effectuer des requêtes de réseau longues, telles que le téléchargement de fichiers volumineux, la synchronisation de données ou la communication avec des API distantes.
  • Les Web Workers peuvent également être utilisés pour lire ou écrire des fichiers volumineux sans bloquer le thread principal de l'application.
  • Enfin, les Web Workers peuvent être utilisés pour effectuer des traitements en temps réel, tels que le traitement de flux vidéo ou audio, sans perturber l'expérience utilisateur.

C'est quoi la différence entre un worker sur un navigateur et un worker dans l'environnement de Node.js ?

Dans le navigateur, les Web Workers sont exécutés dans un environnement isolé, distinct du thread principal. Cet environnement isolé comprend notamment une copie séparée du contexte d'exécution JavaScript, du DOM et des API Web, qui ne sont pas partagées avec le thread principal. Cela permet aux Web Workers de s'exécuter en parallèle avec le thread principal, sans bloquer l'interface utilisateur.

En revanche, dans Node.js, les Web Workers sont exécutés dans un environnement de thread distinct, mais partagent le même contexte d'exécution JavaScript que le thread principal. Cela signifie que les Web Workers dans Node.js peuvent accéder aux mêmes variables et fonctions que le thread principal, ce qui peut faciliter la communication et le partage de ressources entre les différents threads. Chez Node.js, on utilise le module worked threads.

Voici un exemple de code en Node.js pour stocker des informations d'un client SMTP en utilisant un worker :

// smtp-worker.ts

const { parentPort } = require('worker_threads')

// Tableau pour stocker les données des mails SMTP
const smtpData = []

// Fonction pour ajouter des données de mails SMTP
const addSmtpData = (data) => {
  smtpData.push(data)
  parentPort.postMessage(`Données SMTP ajoutées : ${JSON.stringify(data)}`)
}

// Écoute des messages du thread principal
parentPort.on('message', (message) => {
  if (message.type === 'addSmtpData') {
    addSmtpData(message.data)
  }
})
// main.ts

const { Worker } = require('worker_threads')

// Création d'un nouveau worker pour stocker les données de mails SMTP
const smtpWorker = new Worker('./smtp-worker.js')

// Fonction pour ajouter des données de mails SMTP
const addSmtpData = (data) => {
  smtpWorker.postMessage({ type: 'addSmtpData', data })
}

// Ajout de données de mails SMTP
addSmtpData({
  from: 'delponleo@gmail.com',
  to: 'leo.delpon@viacesi.fr',
  subject: 'Yo le man',
  body: 'Comment-vas-tu chef ?',
})
addSmtpData({
  from: 'leo.delpon@viacesi.fr',
  to: 'delponleo@gmail.com',
  subject: 'REPONSE: Yo le man !',
  body: 'Super et toi ?',
})

Une alternative en Node.js, les clusters !

Imaginez que vous ayez un ordinateur avec plusieurs processeurs ou plusieurs cœurs. Vous pourriez penser que Node.js utilise automatiquement tous les processeurs disponibles pour exécuter votre application, mais ce n'est pas le cas. En fait, par défaut, Node.js n'utilise qu'un seul processeur, même si vous en avez plusieurs.

C'est là que le clustering entre en jeu. Le clustering est un module de Node.js qui permet d'exécuter votre application sur plusieurs processus, chacun sur un cœur ou un processeur différent. Cela permet de répartir la charge de votre application sur plusieurs processus, ce qui améliore les performances globales et permet d'exploiter au maximum la puissance de votre ordinateur.

La différence entre le cluster et le worker_thread, la principale différence est que le cluster utilise plusieurs processus distincts pour exécuter votre application, tandis que le worker_thread utilise plusieurs threads dans le même processus. Cela signifie que le cluster est plus adapté aux applications qui nécessitent beaucoup de ressources processeur, tandis que le worker_thread est plus adapté aux applications qui nécessitent beaucoup de ressources en mémoire ou en entrée/sortie.

Il est essentiel de comprendre que le clustering n'est pas toujours la solution optimale pour augmenter les performances de votre application. En effet, l'implémentation du clustering peut être complexe et nécessiter une connaissance avancée des processus et de la concurrence en programmation. De plus, d'autres méthodes d'optimisation telles que l'utilisation de worker_threads peuvent mieux convenir à certaines applications. Il est donc important de bien évaluer les besoins spécifiques de votre application et les ressources dont vous disposez avant de décider quelle méthode d'optimisation utiliser.

Conclusion

Eh bien, il s'avère que JavaScript est bel et bien mono-threaded ! Mais, ne vous inquiétez pas, vous pouvez toujours vous amuser avec le multi-threading en utilisant les outils appropriés. Alors, techniquement, le commentaire était "un peu juste" mais pas très précis. Toutefois, cela nous rappelle l'importance de prendre du recul sur les informations que l'on nous donne (et surtout celles que j'écris !). Chacun a sa propre vision des choses, mais il est primordial de vérifier les informations avant de les croire ou de les répéter aveuglément. En d'autres termes, ne croyez pas tout ce que vous lisez sur Internet, même si cela vient de moi ! c