Skip to main content

线程

在Node.js中,线程是实现高性能和并发的关键概念。由于Node.js主线程是单线程的,理解其线程模型和使用场景对编写高效代码至关重要。以下从基础到高级全面解析:

一、Node.js线程模型概述

Node.js采用单线程事件循环 + 后台线程池的混合模型:

  1. 主线程(事件循环)

    • 执行JavaScript代码和处理事件循环的六个阶段(timers → pending callbacks → poll → check → close callbacks)。
    • 单线程执行,若被CPU密集型任务阻塞,会导致所有异步操作暂停。
  2. libuv线程池

    • 处理无法异步化的I/O操作(如文件读写、DNS解析)和用户提交的CPU密集型任务。
    • 默认大小为4个线程,可通过UV_THREADPOOL_SIZE环境变量调整。

二、libuv线程池的使用场景

线程池自动处理以下操作:

  • 文件系统操作:如fs.readFilefs.writeFile
  • DNS解析:如dns.lookup
  • 加密操作:如crypto.pbkdf2

示例:文件读取(自动使用线程池)

const fs = require('fs');

// 异步读取文件(在线程池中执行)
fs.readFile('large_file.txt', (err, data) => {
if (err) throw err;
console.log('文件读取完成');
});

// 主线程继续执行其他代码
console.log('继续执行...');

三、Worker Threads(工作线程)

Node.js 10+引入的官方多线程API,用于处理CPU密集型任务:

1. 基本用法

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

if (isMainThread) {
// 主线程创建工作线程
const worker = new Worker(__filename, {
workerData: { num: 10 }
});

worker.on('message', (result) => {
console.log('主线程收到结果:', result); // 输出: 55
});

worker.on('error', (err) => {
console.error('工作线程错误:', err);
});

worker.on('exit', (code) => {
console.log(`工作线程退出,退出码: ${code}`);
});
} else {
// 工作线程执行计算
const { num } = workerData;
let sum = 0;
for (let i = 1; i <= num; i++) {
sum += i;
}

// 向主线程发送结果
parentPort.postMessage(sum);
}

2. 多线程计算示例

将大计算任务拆分到多个线程:

const { Worker } = require('worker_threads');

function calculatePrimesInRange(start, end) {
return new Promise((resolve, reject) => {
const worker = new Worker(`
const { parentPort } = require('worker_threads');
function isPrime(num) {
for (let i = 2; i <= Math.sqrt(num); i++) {
if (num % i === 0) return false;
}
return num > 1;
}
const primes = [];
for (let i = ${start}; i <= ${end}; i++) {
if (isPrime(i)) primes.push(i);
}
parentPort.postMessage(primes);
`, { eval: true });

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 findPrimes() {
const [primes1, primes2, primes3, primes4] = await Promise.all([
calculatePrimesInRange(2, 100000),
calculatePrimesInRange(100001, 200000),
calculatePrimesInRange(200001, 300000),
calculatePrimesInRange(300001, 400000)
]);

const allPrimes = [...primes1, ...primes2, ...primes3, ...primes4];
console.log(`找到 ${allPrimes.length} 个素数`);
}

findPrimes();

四、线程池 vs. Worker Threads

特性libuv线程池Worker Threads
创建方式自动创建,默认4个线程手动创建,数量不限
适用场景I/O密集型操作(文件读写、DNS解析)CPU密集型计算(加密、大数据处理)
API复杂度透明使用(无需手动管理)需要手动创建和管理线程
数据传递不支持直接传递通过postMessage传递(拷贝而非共享)
内存隔离不隔离(共享主线程内存)隔离(每个线程有独立V8实例)

五、多线程最佳实践

  1. 避免阻塞主线程

    • CPU密集型任务必须使用Worker Threads,否则会阻塞事件循环:
      // 错误示例:阻塞主线程
      function heavyCalculation() {
      let result = 0;
      for (let i = 0; i < 1e9; i++) {
      result += i;
      }
      return result;
      }

      // 正确做法:使用Worker Threads
      const worker = new Worker(`
      const { parentPort } = require('worker_threads');
      let result = 0;
      for (let i = 0; i < 1e9; i++) {
      result += i;
      }
      parentPort.postMessage(result);
      `, { eval: true });
  2. 优化线程池大小

    • 根据应用特性调整线程池大小:
      # 设置线程池为16个线程
      UV_THREADPOOL_SIZE=16 node app.js
  3. 线程间通信

    • 使用postMessage传递数据,避免共享状态:
      // 主线程
      worker.postMessage({ command: 'process', data: largeArray });

      // 工作线程
      parentPort.on('message', (msg) => {
      if (msg.command === 'process') {
      // 处理数据
      parentPort.postMessage(processData(msg.data));
      }
      });
  4. 错误处理

    • 监听errorexit事件,确保线程异常退出时能捕获:
      worker.on('error', (err) => {
      console.error('工作线程错误:', err);
      });

      worker.on('exit', (code) => {
      if (code !== 0) {
      console.error(`工作线程异常退出,退出码: ${code}`);
      // 可选择重启线程
      }
      });

六、常见误区与注意事项

  1. 认为Node.js完全单线程

    • Node.js主线程是单线程的,但libuv线程池和Worker Threads允许并行执行。
  2. 过度使用线程

    • 创建过多线程会导致上下文切换开销,降低性能。线程数应与CPU核心数匹配。
  3. 共享状态风险

    • Worker Threads间不共享内存,但主线程与Worker可通过SharedArrayBuffer共享内存(需谨慎使用,避免竞态条件):
      // 创建共享内存
      const sab = new SharedArrayBuffer(1024);
      const uint8Array = new Uint8Array(sab);

      // 传递给工作线程
      const worker = new Worker(__filename, { workerData: sab });

通过合理利用线程池和Worker Threads,Node.js能同时处理高并发I/O和CPU密集型任务,充分发挥多核CPU的性能。