Node.js 事件循环与线程池
事件循环和线程池是 Node.js 高效 I/O 的核心。
事件循环
六个阶段
JavaScript
// 事件循环阶段顺序
┌───────────────────────────┐
┌─>│ timers │ setTimeout/setInterval
│ └─────────────────┬─────────┘
│ ┌─────────────────┴─────────┐
│ │ pending callbacks │ I/O 回调
│ └─────────────────┬─────────┘
│ ┌─────────────────┴─────────┐
│ │ idle, prepare │ 内部使用
│ └─────────────────┬─────────┘
│ ┌─────────────────┴─────────┐
│ │ poll │ I/O 事件
│ └─────────────────┬─────────┘
│ ┌─────────────────┴─────────┐
│ │ check │ setImmediate
│ └─────────────────┬─────────┘
│ ┌─────────────────┴─────────┐
└──│ close callbacks │ close 事件
└───────────────────────────┘
各阶段说明
| 阶段 | 说明 | 示例 |
|---|---|---|
| timers | 执行定时器回调 | setTimeout, setInterval |
| pending callbacks | 执行延迟 I/O 回调 | 系统操作回调 |
| idle, prepare | 内部使用 | libuv 内部 |
| poll | 执行 I/O 回调,等待新事件 | fs, net 回调 |
| check | 执行 setImmediate 回调 | setImmediate |
| close callbacks | 执行 close 回调 | socket.destroy |
定时器执行顺序
setTimeout vs setImmediate
JavaScript
// 在 I/O 回调中
fs.readFile('file.txt', () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
});
// 输出顺序:immediate -> timeout
// 因为在 poll 阶段后先执行 check
在主模块中
JavaScript
// 非 I/O 回调中顺序不确定
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// 顺序取决于进程启动性能
process.nextTick
微任务队列
JavaScript
// nextTick 在事件循环每个阶段后执行
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
process.nextTick(() => console.log('nextTick'));
// 输出:nextTick -> timeout/immediate
Promise 微任务
JavaScript
// Promise.then 在 nextTick 之后
process.nextTick(() => console.log('tick'));
Promise.resolve().then(() => console.log('promise'));
console.log('sync');
// 输出:sync -> tick -> promise
线程池
libuv 线程池
JavaScript
// 默认 4 个线程
// 处理以下操作:
// - 文件系统操作
// - DNS 解析
// - 用户自定义任务
// 调整线程数
process.env.UV_THREADPOOL_SIZE = 8;
线程池处理的操作
| 操作 | 是否使用线程池 |
|---|---|
| fs.readFile | 是 |
| fs.writeFile | 是 |
| dns.lookup | 是 |
| crypto.pbkdf2 | 是 |
| zlib 压缩 | 是 |
| net.Socket | 否(系统异步) |
| http.request | 否(系统异步) |
事件循环阻塞
CPU 密集型阻塞
JavaScript
// 阻塞事件循环
function heavyCompute() {
for (let i = 0; i < 1e9; i++) {
// 计算阻塞主线程
}
}
heavyCompute(); // 阻塞所有异步操作
解决方案
JavaScript
// 分片执行
function computeChunked(total, chunkSize, callback) {
let count = 0;
const chunk = () => {
for (let i = 0; i < chunkSize && count < total; i++) {
// 处理一块
count++;
}
if (count < total) {
setImmediate(chunk); // 让出事件循环
} else {
callback();
}
};
chunk();
}
// 或使用 Worker
const { Worker } = require('worker_threads');
监控事件循环延迟
使用 perf_hooks
JavaScript
const { monitorEventLoopDelay } = require('perf_hooks');
const h = monitorEventLoopDelay();
h.enable();
// 定期检查
setInterval(() => {
console.log('延迟:', h.mean, 'ms');
console.log('最大延迟:', h.max, 'ms');
}, 1000);
h.disable();
手动检测
JavaScript
let lastTime = Date.now();
setInterval(() => {
const now = Date.now();
const delay = now - lastTime - 100; // 预期间隔100ms
if (delay > 50) {
console.log('事件循环延迟:', delay, 'ms');
}
lastTime = now;
}, 100);
队列溢出
nextTick 队列溢出
JavaScript
// 递归调用 nextTick 阻塞
function recursiveNextTick() {
process.nextTick(() => {
recursiveNextTick(); // 永不执行 I/O
});
}
setImmediate 替代
JavaScript
// setImmediate 让事件循环继续
function recursiveImmediate() {
setImmediate(() => {
recursiveImmediate(); // 允许 I/O 执行
});
}
注意事项
- 事件循环单线程,CPU 密集型会阻塞
- 定时器不精确,受事件循环影响
- nextTick 优先于 Promise
- I/O 回调中 setImmediate 优于 setTimeout
- 线程池默认 4 线程,可调整
要点总结
- 事件循环六阶段:timers → pending → poll → check → close
process.nextTick最高优先级setImmediate在 I/O 回调中先于setTimeout(0)- libuv 线程池处理文件、DNS、加密
- CPU 密集型使用 Worker 或分片执行
📝 发现内容有误?点击此处直接编辑