Skip to content

Worker Threads

Supposons que vous souhaitiez effectuer une opération mathématique très lourde, comme trouver un grand nombre premier, et que vous l'exécutiez directement dans votre code principal :

javascript
function isPrime(number) {
    for (let i = 2, sqrt = Math.sqrt(number); i <= sqrt; i++) {
        if (number % i === 0) {
            return false;
        }
    }
    return number > 1;
}

function findLargePrime() {
    let num = 21474836478458963245; 
    while (!isPrime(num)) {
        num--;
    }
    console.log(`Le plus grand nombre premier inférieur à 2147483647 est : ${num}`);
}

console.time()
findLargePrime();
console.timeEnd();
function isPrime(number) {
    for (let i = 2, sqrt = Math.sqrt(number); i <= sqrt; i++) {
        if (number % i === 0) {
            return false;
        }
    }
    return number > 1;
}

function findLargePrime() {
    let num = 21474836478458963245; 
    while (!isPrime(num)) {
        num--;
    }
    console.log(`Le plus grand nombre premier inférieur à 2147483647 est : ${num}`);
}

console.time()
findLargePrime();
console.timeEnd();

Lorsque vous exécutez ce code, vous pourriez constater que le message "console.timeEnd()" pourrait prendre beaucoup de temps à s'afficher, voire ne jamais s'afficher si la fonction findLargePrime prend énormément de temps.

Problème:

La fonction findLargePrime est une fonction CPU-intensive. Exécutée dans le thread principal, elle bloquera la boucle d'événements jusqu'à ce qu'elle trouve un nombre premier, empêchant toute autre opération asynchrone ou même des tâches simples comme le journalisation d'être exécutées.

Solution:

Pour éviter de telles situations, vous pouvez utiliser des threads de travail ou des techniques pour diviser les tâches longues en plusieurs petites tâches. Dans ce cas, les worker_threads seraient une solution idéale. Ils permettraient d'exécuter le calcul dans un thread séparé, laissant l'Event Loop du thread principal non bloqué.

worker_threads dans Node.js

Les worker_threads permettent l'exécution de code JavaScript en parallèle dans Node.js. Il est utile pour effectuer des opérations CPU-intensives sans bloquer la boucle d'événements.

Comment utiliser les worker_threads

  1. Initialisation

    Pour utiliser les worker_threads, vous devez d'abord importer les éléments nécessaires du module.

    javascript
    import { Worker, isMainThread, parentPort } from 'worker_threads';
    import { Worker, isMainThread, parentPort } from 'worker_threads';
  2. Déterminer si le code est exécuté dans le thread principal ou dans un worker

    La constante isMainThread est true si le code est exécuté dans le thread principal et false s'il est exécuté dans un worker.

    javascript
    if (isMainThread) {
      // Code du thread principal
    } else {
      // Code du worker
    }
    if (isMainThread) {
      // Code du thread principal
    } else {
      // Code du worker
    }
  3. Création d'un nouveau worker

    Dans le thread principal, vous pouvez créer un nouveau worker en instanciant le Worker avec le chemin de votre fichier script.

    javascript
    const worker = new Worker('./workerCode.js');
    const worker = new Worker('./workerCode.js');

    Ce script de worker (workerCode.js dans cet exemple) sera exécuté dans un thread séparé.

  4. Communication entre le thread principal et le worker

    • Du thread principal vers le worker : Vous pouvez envoyer des messages au worker à l'aide de la méthode postMessage et écouter les messages du worker avec l'événement message.

      javascript
      worker.postMessage('Hello, Worker!');
      worker.on('message', (message) => {
        console.log('Reçu du worker:', message);
      });
      worker.postMessage('Hello, Worker!');
      worker.on('message', (message) => {
        console.log('Reçu du worker:', message);
      });
    • Du worker vers le thread principal : Dans le script du worker, vous pouvez utiliser parentPort pour envoyer et recevoir des messages.

      javascript
      parentPort.on('message', (message) => {
        console.log('Reçu du thread principal:', message);
        parentPort.postMessage('Hello, Main Thread!');
      });
      parentPort.on('message', (message) => {
        console.log('Reçu du thread principal:', message);
        parentPort.postMessage('Hello, Main Thread!');
      });
  5. Gestion des erreurs et terminaison

    Les workers peuvent émettre des erreurs. Vous pouvez écouter l'événement error pour les gérer.

    javascript
    worker.on('error', (error) => {
      console.error('Erreur dans le worker:', error);
    });
    worker.on('error', (error) => {
      console.error('Erreur dans le worker:', error);
    });

    Une fois qu'un worker a terminé son travail, vous pouvez le terminer explicitement ou écouter l'événement exit.

    javascript
    worker.on('exit', (code) => {
      if (code !== 0) {
        console.error(`Worker stopped with exit code ${code}`);
      }
    });
    worker.on('exit', (code) => {
      if (code !== 0) {
        console.error(`Worker stopped with exit code ${code}`);
      }
    });

Exemple

Supposons que vous souhaitiez utiliser un worker pour calculer la somme des nombres jusqu'à un certain nombre.

main.js :

javascript
import { Worker } from 'worker_threads';

const worker = new Worker('./sumWorker.js');

worker.on('message', (sum) => {
  console.log(`Somme: ${sum}`);
});

worker.postMessage(1000000);
import { Worker } from 'worker_threads';

const worker = new Worker('./sumWorker.js');

worker.on('message', (sum) => {
  console.log(`Somme: ${sum}`);
});

worker.postMessage(1000000);

sumWorker.js :

javascript
import { parentPort } from 'worker_threads';

parentPort.on('message', (max) => {
  let sum = 0;
  for (let i = 0; i <= max; i++) {
    sum += i;
  }
  parentPort.postMessage(sum);
});
import { parentPort } from 'worker_threads';

parentPort.on('message', (max) => {
  let sum = 0;
  for (let i = 0; i <= max; i++) {
    sum += i;
  }
  parentPort.postMessage(sum);
});

Lorsque vous exécutez main.js, il enverra le nombre 1000000 au worker. Le worker calculera la somme des nombres jusqu'à ce nombre et renverra le résultat au thread principal.

Les worker_threads sont un excellent moyen de paralléliser des tâches intensives en CPU et d'optimiser l'utilisation des ressources de votre machine, tout en gardant la boucle d'événements de votre application Node.js non bloquée.

Les limitations

L'utilisation des worker_threads dans Node.js vient avec plusieurs avantages, principalement liés à la parallélisation des tâches intensives en CPU. Cependant, il y a aussi des limitations et des précautions à prendre en compte :

  1. Mémoire partagée limitée : Contrairement à d'autres environnements multithread, chaque worker dans Node.js a sa propre mémoire. Les données ne peuvent pas être partagées directement entre les threads. Au lieu de cela, les données sont échangées via des messages, ce qui signifie qu'elles sont sérialisées et désérialisées, ce qui peut avoir un coût en performance pour de grandes quantités de données.

  2. Overhead de création : Il y a un coût associé à la création d'un nouveau thread. Si un worker est utilisé pour une petite tâche qui est exécutée rapidement, l'overhead de création du thread peut surpasser les avantages d'exécuter cette tâche hors du thread principal.

  3. Complexité accrue : La gestion de plusieurs threads peut rendre le code plus complexe, surtout en ce qui concerne la coordination entre les threads, la gestion des erreurs, etc.

  4. Limitation des ressources : Même si chaque worker s'exécute dans un thread distinct, le nombre de threads que vous pouvez créer est limité par les ressources système (par exemple, la mémoire ou le nombre maximal de threads). Créer un grand nombre de threads peut saturer les ressources et nuire aux performances.

  5. Pas de garantie d'ordre d'exécution : Puisque les workers s'exécutent en parallèle, il n'y a aucune garantie sur l'ordre d'exécution ou de finition des tâches. Vous devez être prudent lorsque vous dépendez de l'ordre des opérations entre les threads.

  6. Modules natifs : Certains modules natifs de Node.js peuvent ne pas être thread-safe. Si un module natif n'est pas conçu pour être utilisé avec worker_threads, cela pourrait conduire à des comportements imprévisibles ou à des erreurs.

  7. Accès limité aux modules globaux : Dans un worker, certains objets et fonctions globaux du thread principal ne sont pas disponibles. Par exemple, console est disponible, mais process.stdout ne l'est pas.

  8. Communication inter-thread : La communication entre le thread principal et les workers est basée sur un système de messagerie. Bien que cela isole bien chaque thread, cela signifie également que les structures de données complexes doivent être copiées ou transférées entre les threads, ce qui peut augmenter l'overhead.

  9. Modules spécifiques au thread principal : Certains modules ou fonctionnalités, comme l'accès à la boucle d'événements, ne sont accessibles qu'à partir du thread principal.