非阻塞I/O模型
非阻塞I/O模型
Node.js的核心特性之一就是非阻塞I/O模型,这种模型使得Node.js能够高效处理大量并发请求。非阻塞I/O意味着当一个I/O操作开始后,程序不会等待操作完成,而是继续执行后续代码,等到I/O操作完成后再通过回调函数处理结果。
阻塞与非阻塞的区别
传统的阻塞I/O模型中,当程序执行一个I/O操作时,线程会被阻塞,直到操作完成。例如读取文件时,程序会停下来等待文件读取完毕:
// 阻塞I/O示例(伪代码)
const data = fs.readFileSync('file.txt'); // 线程在这里阻塞
console.log(data);
console.log('程序继续执行');
而非阻塞I/O模型则完全不同:
// 非阻塞I/O示例
fs.readFile('file.txt', (err, data) => {
if (err) throw err;
console.log(data);
});
console.log('程序继续执行');
在这个例子中,console.log('程序继续执行')
会立即执行,不必等待文件读取完成。
事件循环机制
Node.js实现非阻塞I/O的关键在于其事件循环机制。事件循环是一个持续运行的进程,检查事件队列并执行相应的回调函数。整个过程可以分为几个阶段:
- 定时器阶段:执行setTimeout和setInterval回调
- I/O回调阶段:执行大部分I/O回调
- 闲置/准备阶段:内部使用
- 轮询阶段:检索新的I/O事件
- 检查阶段:执行setImmediate回调
- 关闭回调阶段:执行关闭事件的回调
// 事件循环示例
setTimeout(() => {
console.log('定时器回调');
}, 0);
fs.readFile('file.txt', () => {
console.log('文件读取回调');
setImmediate(() => {
console.log('setImmediate回调');
});
});
console.log('主线程代码');
输出顺序将是:主线程代码 → 定时器回调 → 文件读取回调 → setImmediate回调
回调地狱与解决方案
虽然非阻塞I/O提高了性能,但嵌套的回调函数可能导致"回调地狱":
fs.readFile('file1.txt', (err, data1) => {
if (err) throw err;
fs.readFile('file2.txt', (err, data2) => {
if (err) throw err;
fs.writeFile('output.txt', data1 + data2, (err) => {
if (err) throw err;
console.log('操作完成');
});
});
});
Promise解决方案
const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);
readFile('file1.txt')
.then(data1 => readFile('file2.txt').then(data2 => [data1, data2]))
.then(([data1, data2]) => writeFile('output.txt', data1 + data2))
.then(() => console.log('操作完成'))
.catch(err => console.error(err));
async/await解决方案
async function processFiles() {
try {
const data1 = await readFile('file1.txt');
const data2 = await readFile('file2.txt');
await writeFile('output.txt', data1 + data2);
console.log('操作完成');
} catch (err) {
console.error(err);
}
}
非阻塞I/O的实际应用
HTTP服务器
Node.js的HTTP服务器是非阻塞I/O的典型应用:
const http = require('http');
const server = http.createServer((req, res) => {
// 模拟耗时I/O操作
setTimeout(() => {
res.end('Hello World');
}, 1000);
console.log('请求处理中...');
});
server.listen(3000, () => {
console.log('服务器运行在 http://localhost:3000');
});
这个服务器可以同时处理多个请求,不会因为一个请求的I/O操作而阻塞其他请求。
数据库操作
非阻塞I/O特别适合数据库操作:
const MongoClient = require('mongodb').MongoClient;
async function getUsers() {
const client = await MongoClient.connect('mongodb://localhost:27017');
const db = client.db('mydb');
const users = await db.collection('users').find().toArray();
client.close();
return users;
}
// 同时处理多个数据库查询
Promise.all([getUsers(), getProducts()])
.then(([users, products]) => {
console.log({ users, products });
});
性能考量
非阻塞I/O模型虽然高效,但也需要注意:
- CPU密集型任务:Node.js不适合处理CPU密集型任务,会阻塞事件循环
- 内存使用:大量并发连接会消耗较多内存
- 错误处理:必须妥善处理回调中的错误,否则可能导致内存泄漏
// 错误的错误处理方式
fs.readFile('file.txt', (err, data) => {
// 忘记处理err
console.log(data);
});
// 正确的错误处理方式
fs.readFile('file.txt', (err, data) => {
if (err) {
console.error('读取文件出错:', err);
return;
}
console.log(data);
});
流处理
Node.js的流(Stream)是非阻塞I/O的高级应用,特别适合处理大文件:
const fs = require('fs');
// 传统方式(内存消耗大)
fs.readFile('largefile.txt', (err, data) => {
// 整个文件被读入内存
});
// 流方式(内存高效)
const readStream = fs.createReadStream('largefile.txt');
const writeStream = fs.createWriteStream('output.txt');
readStream.on('data', (chunk) => {
console.log(`接收到 ${chunk.length} 字节数据`);
writeStream.write(chunk);
});
readStream.on('end', () => {
writeStream.end();
console.log('文件传输完成');
});
工作线程与非阻塞I/O
对于CPU密集型任务,可以使用Worker Threads来避免阻塞事件循环:
const { Worker } = require('worker_threads');
function runService(workerData) {
return new Promise((resolve, reject) => {
const worker = new Worker('./worker.js', { workerData });
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Worker stopped with exit code ${code}`));
}
});
});
}
// worker.js
const { workerData, parentPort } = require('worker_threads');
// CPU密集型任务
function heavyComputation(data) {
// ...复杂计算
return result;
}
const result = heavyComputation(workerData);
parentPort.postMessage(result);
调试非阻塞代码
调试异步代码可能比较困难,可以使用async_hooks模块跟踪异步操作:
const async_hooks = require('async_hooks');
const fs = require('fs');
// 跟踪异步资源
const asyncHook = async_hooks.createHook({
init(asyncId, type, triggerAsyncId) {
fs.writeSync(1, `Init: ${type} asyncId: ${asyncId}\n`);
},
destroy(asyncId) {
fs.writeSync(1, `Destroy: ${asyncId}\n`);
}
});
asyncHook.enable();
setTimeout(() => {
console.log('异步操作完成');
}, 100);
最佳实践
- 避免阻塞事件循环:将CPU密集型任务委托给工作线程或子进程
- 合理使用Promise:避免不必要的Promise链
- 错误处理:始终处理Promise拒绝和回调错误
- 流处理:对大文件使用流而非一次性读取
- 并发控制:使用类似p-limit的库控制并发量
const pLimit = require('p-limit');
const limit = pLimit(3); // 最大并发数3
async function downloadAll(urls) {
const promises = urls.map(url =>
limit(() => download(url))
);
return Promise.all(promises);
}
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn