Lesson 04-Processes and Threads in Node.js

Processes

Executing External Applications

Basic Concepts

  • Four Asynchronous Methods: exec, execFile, fork, spawn
  • Node.js:
    • fork: Used to run a Node.js process as an independent process, detaching computation and file descriptors from the main Node.js process.
  • Non-Node.js:
    • spawn: Ideal for scenarios with many child process I/O operations or large outputs.
    • execFile: Used for executing a single external program; it is fast and relatively safe for handling user input.
    • exec: Used for direct access to shell commands; caution is needed with user input.
  • Three Synchronous Methods: execSync, execFileSync, spawnSync
  • Child processes created via these APIs have no inherent connection to the parent process.

execFile

  • Buffers the output and returns the final result or error information via a callback.
const cp = require('child_process');

cp.execFile('echo', ['hello', 'world'], (err, stdout, stderr) => {
  if (err) {
    console.error(err);
  }
  console.log('stdout: ', stdout);
  console.log('stderr: ', stderr);
});

spawn

  • Uses streams to handle external applications with large data outputs, saving memory.
  • Improves data response efficiency through streaming.
  • The spawn method returns a stream interface for I/O operations.
Single Task
const cp = require('child_process');

const child = cp.spawn('echo', ['hello', 'world']);
child.on('error', console.error);
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
Multi-Task Pipeline
const cp = require('child_process');
const path = require('path');

const cat = cp.spawn('cat', [path.resolve(__dirname, 'messy.txt')]);
const sort = cp.spawn('sort');
const uniq = cp.spawn('uniq');

cat.stdout.pipe(sort.stdin);
sort.stdout.pipe(uniq.stdin);
uniq.stdout.pipe(process.stdout);

exec

  • Accepts a single string command.
  • Behaves identically to shell execution.
const cp = require('child_process');

cp.exec(`cat ${__dirname}/messy.txt | sort | uniq`, (err, stdout, stderr) => {
  console.log(stdout);
});

fork

  • The fork method establishes an IPC (Inter-Process Communication) channel for message passing between Node.js processes.
  • A child process typically takes 30ms to start and consumes 10MB of memory.
  • Child Process: Uses process.on('message') and process.send().
  • Parent Process: Uses child.on('message') and child.send().
Parent-Child Process Communication
// parent.js
const cp = require('child_process');

const child = cp.fork('./child', { silent: true });
child.send('monkeys');
child.on('message', function (message) {
  console.log('got message from child', message, typeof message);
});
child.stdout.pipe(process.stdout);

setTimeout(function () {
  child.disconnect();
}, 3000);
// child.js
process.on('message', function (message) {
  console.log('got one', message);
  process.send('no pizza');
  process.send(1);
  process.send({ my: 'object' });
  process.send(false);
  process.send(null);
});

console.log(process);

Common Techniques

Terminating All Child Processes on Exit

  • Maintain references to ChildProcess objects returned by spawn and terminate them when the main process exits.
const spawn = require('child_process').spawn;
const children = [];

process.on('exit', function () {
  console.log('killing', children.length, 'child processes');
  children.forEach(function (child) {
    child.kill();
  });
});

children.push(spawn('/bin/sleep', ['10']));
children.push(spawn('/bin/sleep', ['10']));
children.push(spawn('/bin/sleep', ['10']));

setTimeout(function () {
  process.exit(0);
}, 3000);

Understanding the Cluster Module

  • Addresses Node.js’s single-process limitation in utilizing multi-core CPUs.
  • The master-worker (cluster) model enhances application robustness.
  • The Cluster module is built on the child_process module, supporting not only ordinary messages but also low-level objects like TCP and UDP.
  • When a TCP connection is sent from the master to a worker, the worker can reconstruct the TCP connection based on the message. The Cluster module can determine the appropriate number of worker processes based on hardware resources.

Threads

Single-Threading Issues

  • Inefficient CPU utilization.
  • An uncaught exception can cause the entire program to crash.

Node.js Threads

  • A Node.js process uses seven threads.
  • The core component is the V8 engine. When Node.js starts, it creates a V8 instance, which is multi-threaded:
  • Main Thread: Compiles and executes code.
  • Compile/Optimize Thread: Optimizes code while the main thread executes.
  • Profiler Thread: Tracks code execution time to provide data for Crankshaft optimization.
  • Garbage Collection Threads: Handle memory cleanup.
  • JavaScript execution in Node.js is single-threaded, but the host environment (Node.js or browsers) is multi-threaded.

Asynchronous I/O

  • Node.js uses a thread pool for certain I/O operations (e.g., DNS, FS) and CPU-intensive tasks (e.g., Zlib, Crypto).
  • The default thread pool size is 4, but it can be modified manually.
process.env.UV_THREADPOOL_SIZE = 64;

Cluster Multi-Process Model

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  console.log(`Master process ${process.pid} is running`);
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`Worker process ${worker.process.pid} exited`);
  });
} else {
  // Workers can share any TCP connection.
  // In this example, they share an HTTP server.
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('Hello World');
  }).listen(8000);
  console.log(`Worker process ${process.pid} started`);
}
  • The example creates 9 processes: 1 master process and 8 worker processes (2 CPUs × 4 cores = 8 workers).
  • Both child_process and cluster use a multi-process model, not a multi-threaded model.
  • To address single-threading limitations, multi-process approaches are commonly used to simulate multi-threading.

True Multi-Threading in Node.js

  • Node.js 10.5.0 introduced the experimental worker_threads module, providing true multi-threading capabilities.
  • The worker_threads module includes four objects and two classes:
  • isMainThread: Indicates whether the current thread is the main thread (determined by threadId === 0 in the source code).
  • MessagePort: Used for inter-thread communication, inheriting from EventEmitter.
  • MessageChannel: Creates an asynchronous, bidirectional communication channel.
  • threadId: The thread’s ID.
  • Worker: Creates a worker thread from the main thread. The first parameter, filename, specifies the worker’s entry point.
  • parentPort: A MessagePort object representing the parent in a worker thread; null in the main thread.
  • workerData: Used to pass data (as a copy) from the main thread to a worker thread.
const {
  isMainThread,
  parentPort,
  workerData,
  threadId,
  MessageChannel,
  MessagePort,
  Worker
} = require('worker_threads');

function mainThread() {
  for (let i = 0; i < 5; i++) {
    const worker = new Worker(__filename, { workerData: i });
    worker.on('exit', code => {
      console.log(`main: worker stopped with exit code ${code}`);
    });
    worker.on('message', msg => {
      console.log(`main: receive ${msg}`);
      worker.postMessage(msg + 1);
    });
  }
}

function workerThread() {
  console.log(`worker: workerData ${workerData}`);
  parentPort.on('message', msg => {
    console.log(`worker: receive ${msg}`);
  });
  parentPort.postMessage(workerData);
}

if (isMainThread) {
  mainThread();
} else {
  workerThread();
}

Thread Communication

const assert = require('assert');
const {
  Worker,
  MessageChannel,
  MessagePort,
  isMainThread,
  parentPort
} = require('worker_threads');

if (isMainThread) {
  const worker = new Worker(__filename);
  const subChannel = new MessageChannel();
  worker.postMessage({ hereIsYourPort: subChannel.port1 }, [subChannel.port1]);
  subChannel.port2.on('message', (value) => {
    console.log('received:', value);
  });
} else {
  parentPort.once('message', (value) => {
    assert(value.hereIsYourPort instanceof MessagePort);
    value.hereIsYourPort.postMessage('the worker is sending this');
    value.hereIsYourPort.close();
  });
}

Multi-Process vs. Multi-Thread

  • Process: The smallest unit of resource allocation.
  • Thread: The smallest unit of CPU scheduling.
Share your love