jsrosetta

Bất đồng bộ

Concurrency: luồng và tiến trình con

worker_threads/child_process.fork() của Node.js so với goroutine (Go), std::thread (Rust), TaskGroup (Swift) và thread/ProcessBuilder (Java).

Phiên bản tối thiểu
Node.js ≥ 12.20Go ≥ 1.25Rust ≥ 1.71Swift ≥ 5.7Java ≥ 25
Đã chạy thử trên
Node.js 24.12.0Go 1.27.1Rust 1.98.1Swift 6.2.4Java 25.0.4.1

Code Node.js là ES module: lưu file .mjs hoặc đặt "type": "module" trong package.json.

Hai bài toán khác nhau: chia việc nặng CPU cho nhiều luồng để chạy song song, và cô lập một tác vụ trong tiến trình riêng. Node.js giải quyết bằng worker_threads và child_process.fork(). Go, Rust và Java dùng goroutine/thread cho vế đầu; Swift dùng TaskGroup — cùng ý tưởng với async let ở bài async/await, nhưng nhận một danh sách công việc động thay vì vài biến cố định. Không ngôn ngữ nào trong bốn ngôn ngữ còn lại có fork() an toàn để cô lập tiến trình: runtime của chúng vốn đa luồng (goroutine scheduler, thread pool async, v.v.), nên fork() chỉ nhân bản luồng gọi nó, để lại mọi lock và luồng khác ở trạng thái không nhất quán trong tiến trình con. Tự thực thi lại chính binary/tiến trình là cách thay thế an toàn ở cả bốn.

Chia việc CPU-bound cho nhiều luồng (worker_threads vs goroutine)

import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads";
import { fileURLToPath } from "node:url";
 
const RANGE_END = 50_000_000;
const WORKER_COUNT = 4;
 
function sumRange(start, end) {
  let total = 0;
  for (let i = start; i < end; i++) {
    total += i;
  }
  return total;
}
 
async function main() {
  const filename = fileURLToPath(import.meta.url);
  const chunk = Math.ceil(RANGE_END / WORKER_COUNT);
 
  const partials = await Promise.all(
    Array.from({ length: WORKER_COUNT }, (_, i) => {
      const start = i * chunk;
      const end = Math.min(start + chunk, RANGE_END);
      return new Promise((resolve, reject) => {
        const worker = new Worker(filename, { workerData: { start, end } });
        worker.on("message", resolve);
        worker.on("error", reject);
      });
    }),
  );
 
  const sum = partials.reduce((total, partial) => total + partial, 0);
  console.log("sum:", sum); // → sum: 1249999975000000
}
 
if (isMainThread) {
  main();
} else {
  const { start, end } = workerData;
  parentPort.postMessage(sumRange(start, end));
}

Cô lập một tác vụ trong tiến trình riêng (fork vs re-exec)

import { fork } from "node:child_process";
import { fileURLToPath } from "node:url";
 
if (process.send) {
  // chạy với tư cách tiến trình con vừa fork
  process.once("message", ({ numbers }) => {
    const results = numbers.map((n) => n * n);
    process.send({ results });
    process.disconnect();
  });
} else {
  // chạy với tư cách tiến trình cha
  const child = fork(fileURLToPath(import.meta.url));
  const numbers = [1, 2, 3, 4, 5];
 
  child.once("message", ({ results }) => {
    console.log("squares:", results.join(", ")); // → squares: 1, 4, 9, 16, 25
    console.log("sum:", results.reduce((a, b) => a + b, 0)); // → sum: 55
  });
 
  child.send({ numbers });
}

Khác biệt chính

Node.js Go Rust Swift Java
Đơn vị song song Worker (V8 isolate riêng) goroutine std::thread Task (trong TaskGroup) Thread (platform/virtual)
Số luồng OS thực tế chạy 1 luồng/worker tối đa GOMAXPROCS luồng dùng chung 1 luồng OS/thread ghép kênh trên global executor 1 luồng/platform thread
Cô lập tiến trình child_process.fork() tự re-exec chính binary tự re-exec (Command::new(current_exe)) Process (Foundation) re-exec ProcessBuilder re-exec

Tham khảo: github.com/miguelmota/golang-for-nodejs-developers#concurrency