Node.js 异步I/O与回调
Node.js 的非阻塞 I/O 模型是其高性能的核心,理解底层机制有助于性能优化。
I/O 模型架构
JavaScript
JavaScript 代码
↓
Node.js 绑定层(C++)
↓
libuv 事件循环
↓
┌─────────────────────┐
│ 系统调用 │
│ ├─ epoll (Linux) │
│ ├─ kqueue (macOS) │
│ ├─ IOCP (Windows) │
│ └─ 线程池 │
└─────────────────────┘
libuv 线程池
JavaScript
// 默认线程池大小:4
process.env.UV_THREADPOOL_SIZE = 8; // 设置为 8
// 线程池处理的操作:
// - fs 文件操作
// - crypto 加密操作
// - zlib 压缩操作
// - dns 解析
// - 用户自定义异步工作
线程池瓶颈
JavaScript
// 4 个线程并发执行文件操作
const fs = require('fs');
// 同时发起 8 个文件读取
for (let i = 0; i < 8; i++) {
fs.readFile(`file${i}.txt`, (err, data) => {
console.log(`file${i} 完成`);
});
}
// 只有 4 个并发执行,其余排队等待
// 增加 UV_THREADPOOL_SIZE 可提高并发度
I/O 执行流程
JavaScript
fs.readFile('file.txt', (err, data) => {
console.log('文件读取完成');
});
// 执行流程:
// 1. JavaScript 调用 fs.readFile
// 2. Node.js 绑定层创建 I/O 请求
// 3. libuv 提交到线程池
// 4. 线程执行系统调用
// 5. 完成后回调进入事件循环队列
// 6. poll 阶段执行回调
poll 阶段详解
JavaScript
// poll 阶段行为
// 1. 获取已完成的 I/O 事件
// 2. 执行对应的回调函数
// 3. 决定阻塞时间或进入下一阶段
// poll 阶段阻塞时间计算
// - 有 setImmediate:不阻塞,进入 check
// - 有到期定时器:不阻塞,进入 timers
// - 有活跃 handles:等待直到最近定时器到期
// - 无活跃 handles:可能退出循环
I/O 回调执行顺序
JavaScript
// I/O 回调在 poll 阶段执行
fs.readFile('file.txt', (err, data) => {
console.log('I/O 回调');
// I/O 回调中触发定时器
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// 输出: I/O 回调 -> immediate -> timeout
// I/O 回调在 poll 阶段,下一阶段是 check
});
同步 vs 异步 I/O
JavaScript
// 同步 I/O(阻塞)
const data = fs.readFileSync('file.txt');
// 线程阻塞,等待操作完成
// 异步 I/O(非阻塞)
fs.readFile('file.txt', (err, data) => {});
// 主线程继续,回调稍后执行
| 方式 | 线程行为 | 适用场景 |
|---|---|---|
| 同步 | 阻塞等待 | 启动时加载配置 |
| 异步 | 不阻塞 | 大量并发请求 |
Error-First 回调规范
JavaScript
// Node.js 回调规范:第一个参数是错误
fs.readFile('file.txt', (err, data) => {
if (err) {
// 处理错误
console.error(err);
return;
}
// 处理数据
console.log(data);
});
// 自定义异步函数遵循规范
function asyncOperation(callback) {
performTask((result, error) => {
if (error) {
callback(error);
} else {
callback(null, result);
}
});
}
I/O 性能优化
批量操作
JavaScript
// ❌ 逐个读取
for (const file of files) {
fs.readFile(file, callback);
}
// ✅ 并发读取
const promises = files.map(f =>
fs.promises.readFile(f)
);
await Promise.all(promises);
流式处理
JavaScript
// ❌ 一次性读取大文件
fs.readFile('large.txt', callback);
// ✅ 流式处理
const stream = fs.createReadStream('large.txt');
stream.on('data', chunk => process(chunk));
stream.on('end', () => console.log('完成'));
缓存策略
JavaScript
// 缓存读取结果
const cache = new Map();
function cachedRead(path) {
if (cache.has(path)) {
return Promise.resolve(cache.get(path));
}
return fs.promises.readFile(path).then(data => {
cache.set(path, data);
return data;
});
}
监控 I/O 性能
JavaScript
// 使用 async_hooks 监控
const asyncHooks = require('async_hooks');
const hook = asyncHooks.createHook({
init(asyncId, type, triggerAsyncId) {
if (type === 'FSREQCALLBACK') {
console.log('I/O 发起:', asyncId);
}
},
destroy(asyncId) {
console.log('I/O 完成:', asyncId);
}
});
hook.enable();
非线程池 I/O
text
// 网络 I/O 不使用线程池
// 直接使用系统事件机制(epoll/kqueue/IOCP)
const net = require('net');
const server = net.createServer();
server.listen(3000);
// 使用系统原生异步机制,不占用线程池
要点总结
- libuv 线程池默认 4 线程,处理 fs/crypto/zlib
- UV_THREADPOOL_SIZE 可调整线程池大小
- I/O 回调在 poll 阶段执行
- I/O 回调中 setImmediate 优先于 setTimeout
- 网络操作不使用线程池,使用系统原生机制
- 流式处理大文件,避免一次性读取
📝 发现内容有误?点击此处直接编辑