Worker Pools in JavaScript

In this example, we’ll look at how to implement a worker pool using JavaScript’s asynchronous features.

const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');

// Here's the worker, of which we'll run several concurrent instances.
// These workers will receive work on the `workerData` and send the corresponding
// results back to the main thread. We'll use a setTimeout to simulate an expensive task.
if (!isMainThread) {
  const worker = async (id, job) => {
    console.log(`worker ${id} started job ${job}`);
    await new Promise(resolve => setTimeout(resolve, 1000));
    console.log(`worker ${id} finished job ${job}`);
    parentPort.postMessage(job * 2);
  };

  worker(workerData.id, workerData.job);
}

// Main thread code
if (isMainThread) {
  const numJobs = 5;
  const numWorkers = 3;
  let completedJobs = 0;

  // This function creates a new worker
  const createWorker = (id, job) => {
    return new Promise((resolve) => {
      const worker = new Worker(__filename, {
        workerData: { id, job }
      });
      worker.on('message', (result) => {
        console.log(`Job ${job} returned ${result}`);
        completedJobs++;
        if (completedJobs === numJobs) {
          console.log('All jobs completed');
        }
        resolve(result);
      });
    });
  };

  // This starts up our worker pool
  const runJobs = async () => {
    const workers = [];
    for (let i = 1; i <= numJobs; i++) {
      const workerId = (i - 1) % numWorkers + 1;
      workers.push(createWorker(workerId, i));
      
      // If we've queued up enough jobs to fill the worker pool,
      // wait for one to complete before queueing more
      if (workers.length === numWorkers) {
        await Promise.race(workers);
        workers.length = 0;
      }
    }
    
    // Wait for any remaining jobs
    await Promise.all(workers);
  };

  console.time('Jobs completed in');
  runJobs().then(() => console.timeEnd('Jobs completed in'));
}

In this JavaScript implementation, we use the worker_threads module to create a worker pool. The main differences from the original example are:

  1. Instead of channels, we use the Worker class and message passing.
  2. We simulate the job queue by creating workers on demand, up to the maximum number of workers.
  3. We use Promises to handle the asynchronous nature of the workers.

To run the program, save it as worker-pools.js and use node:

$ node worker-pools.js
worker 1 started job 1
worker 2 started job 2
worker 3 started job 3
worker 1 finished job 1
Job 1 returned 2
worker 1 started job 4
worker 2 finished job 2
Job 2 returned 4
worker 2 started job 5
worker 3 finished job 3
Job 3 returned 6
worker 1 finished job 4
Job 4 returned 8
worker 2 finished job 5
Job 5 returned 10
All jobs completed
Jobs completed in: 2.023s

Our running program shows the 5 jobs being executed by various workers. The program only takes about 2 seconds despite doing about 5 seconds of total work because there are 3 workers operating concurrently.

This example demonstrates how to implement a worker pool pattern in JavaScript, which can be useful for distributing CPU-intensive tasks across multiple threads.