Concurrence et multithreading en TypeScript : de la théorie à la pratique
Les applications web modernes sont de plus en plus confrontées à la nécessité de traiter de grands volumes de données et d'effectuer des opérations gourmandes en ressources sans bloquer l'interface utilisateur. TypeScript, étant un sur-ensemble de JavaScript, hérite de son modèle d'exécution monothread, mais offre en même temps des outils puissants pour organiser la concurrence et le multithreading. Dans cet article, nous allons voir comment utiliser efficacement ces possibilités dans vos projets.
Comprendre la concurrence et le multithreading
Avant de plonger dans le code, il est important de distinguer deux concepts clés :
- Concurrence — la capacité d'un système à traiter plusieurs tâches dans des intervalles de temps qui se chevauchent, mais pas nécessairement simultanément. Ceci est réalisé grâce à un changement rapide de contexte.
- Multithreading — l'exécution parallèle de plusieurs threads sur différents cœurs du processeur. Dans Node.js, cela est implémenté via les Worker Threads.
TypeScript fonctionne dans l'environnement Node.js, où le thread principal traite les événements via l'Event Loop. Pour les calculs lourds, cela peut devenir un goulot d'étranglement.
Asynchronisme et Promise : le fondement de la concurrence
Le niveau de base de la concurrence en TypeScript repose sur async/await et Promise. Ces mécanismes permettent de ne pas bloquer le thread principal pendant l'attente des opérations I/O.
// Exemple de chargement asynchrone de donnéesinterface User { id: number; name: string;}
async function fetchUser(id: number): Promise<User> { const response = await fetch(`https://api.example.com/users/${id}`); return response.json();}
// Lancement parallèle de plusieurs requêtesasync function loadUsers(): Promise<User[]> { const userIds = [1, 2, 3, 4, 5]; const promises = userIds.map(id => fetchUser(id)); return Promise.all(promises);}
loadUsers().then(users => { console.log('Utilisateurs chargés :', users.length);});Promise.all lance toutes les promesses simultanément, mais elles s'exécutent dans un seul thread. C'est idéal pour les tâches I/O, mais ne convient pas pour les opérations intensives en CPU.
Worker Threads : le véritable multithreading
Pour effectuer des calculs lourds sans bloquer le thread principal, utilisez Worker Threads. C'est un module intégré de Node.js qui crée des threads séparés avec leur propre mémoire.
Créons un exemple de calcul des nombres de Fibonacci dans un thread séparé :
// fibonacci.worker.tsimport { parentPort } from 'worker_threads';
function fibonacci(n: number): number { if (n <= 1) return n; return fibonacci(n - 1) + fibonacci(n - 2);}
parentPort?.on('message', (n: number) => { const result = fibonacci(n); parentPort?.postMessage(result);});// main.tsimport { Worker } from 'worker_threads';import path from 'path';
function runFibonacciInWorker(n: number): Promise<number> { return new Promise((resolve, reject) => { const worker = new Worker(path.resolve(__dirname, 'fibonacci.worker.ts')); worker.postMessage(n); worker.on('message', resolve); worker.on('error', reject); worker.on('exit', (code) => { if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`)); }); });}
async function main() { console.time('fibonacci'); const result = await runFibonacciInWorker(40); console.timeEnd('fibonacci'); console.log(`Fibonacci(40) = ${result}`);}
main();Notez : pour exécuter les workers TypeScript, une compilation en JavaScript sera nécessaire. Vous pouvez utiliser ts-node ou une compilation préalable.
SharedArrayBuffer : mémoire partagée entre les threads
Lorsque les threads ont besoin d'échanger de grands volumes de données