Concurrence et multithreading en TypeScript : de A à Z
Dans le monde moderne du développement web, la performance des applications devient primordiale. TypeScript, en tant que surcouche fortement typée de JavaScript, offre aux développeurs des outils puissants pour travailler avec l'asynchronisme et le multithreading. Dans cet article, nous allons explorer les concepts fondamentaux de la concurrence, comprendre comment fonctionne l'Event Loop, et apprendre à utiliser les Worker Threads pour un véritable multithreading dans Node.js.
1. Asynchronisme et Event Loop : le fondement de la concurrence en TypeScript
Avant de parler de multithreading, il est important de comprendre comment TypeScript (et JavaScript) gère les tâches concurrentes. À la base se trouve l'Event Loop — un mécanisme qui permet d'exécuter des opérations d'entrée-sortie non bloquantes sans créer de nouveaux threads.
Prenons un exemple simple de code asynchrone :
async function fetchData(url: string): Promise<string> { const response = await fetch(url); return response.text();}
async function main(): Promise<void> { console.log('Début du chargement'); const data = await fetchData('https://api.example.com/data'); console.log('Données reçues :', data);}
main();console.log('Le code continue de s\'exécuter');Dans cet exemple, await ne bloque pas le thread principal. Pendant l'attente de la réponse du serveur, l'Event Loop continue de traiter d'autres tâches, comme l'exécution de console.log. C'est ce qu'on appelle la concurrence — la capacité à traiter plusieurs tâches dans un même intervalle de temps en basculant entre elles.
2. Promesses et async/await : gestion des opérations asynchrones
TypeScript fournit des constructions syntaxiques pratiques pour travailler avec l'asynchronisme. Les Promesses (Promises) sont des objets représentant le résultat futur d'une opération asynchrone. La combinaison async/await rend le code linéaire et lisible.
Examinons un scénario plus complexe — l'exécution parallèle de plusieurs requêtes :
async function loadMultipleUsers(userIds: number[]): Promise<User[]> { const promises = userIds.map(id => fetch(`/users/${id}`).then(res => res.json())); return Promise.all(promises);}
// Utilisationconst users = await loadMultipleUsers([1, 2, 3, 4, 5]);console.log(users); // Tous les utilisateurs sont chargés en parallèleLa méthode Promise.all lance toutes les promesses simultanément, ce qui accélère considérablement l'exécution par rapport à un chargement séquentiel. C'est un exemple parfait de concurrence : les tâches sont exécutées « simultanément » dans un seul thread.
3. Véritable multithreading : Worker Threads dans Node.js
Bien que l'asynchronisme gère très bien les opérations d'entrée-sortie, il n'aide pas pour les tâches intensives en CPU (traitement d'images, calculs complexes, chiffrement). Pour ces cas, Node.js propose les Worker Threads — de véritables threads du système d'exploitation.
Prenons un exemple de calcul de factorielle dans un thread séparé :
// worker.tsimport { parentPort } from 'worker_threads';
function factorial(n: number): number { if (n === 0) return 1; return n * factorial(n - 1);}
parentPort?.on('message', (n: number) => { const result = factorial(n); parentPort?.postMessage(result);});
// main.tsimport { Worker } from 'worker_threads';
function runWorker(n: number): Promise<number> { return new Promise((resolve, reject) => { const worker = new Worker('./worker.ts'); worker.postMessage(n); worker.on('message', resolve); worker.on('error', reject); });}
async function main() { const result = await runWorker(20); console.log('Factorielle de 20 :', result);}
main();Chaque Worker fonctionne dans son propre thread, possède sa propre Event Loop et est