Node.js 异步处理机制详解
你的问题解答
问题:Express接口需要3秒返回,后面的请求会阻塞吗?
答案:不会阻塞! 这正是Node.js的核心优势。
Node.js 如何处理并发请求
1. 事件循环 (Event Loop)
Node.js虽然是单线程的,但它使用了事件循环机制:
┌───────────────────────────┐
┌─>│ timers │ ← setTimeout, setInterval
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │ ← I/O回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │ ← 内部使用
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ poll │ ← 获取新的I/O事件
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ check │ ← setImmediate回调
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │ ← 关闭回调
└───────────────────────────┘2. 非阻塞I/O原理
当一个请求到达时:
- 主线程接收请求
- 如果是异步操作(如setTimeout、数据库查询、文件读取)
- 将操作委托给线程池或系统
- 主线程继续处理下一个请求
- 异步操作完成后,回调被放入事件队列
- 事件循环检查队列,执行回调
3. 实际执行流程
javascript
// 当请求1到达 /slow 接口
app.get('/slow', async (req, res) => {
console.log('请求1开始处理');
// setTimeout 不会阻塞主线程
await new Promise(resolve => setTimeout(resolve, 3000));
console.log('请求1处理完成');
res.json({message: '完成'});
});
// 在请求1等待的3秒内,请求2到达 /fast 接口
app.get('/fast', (req, res) => {
console.log('请求2立即处理'); // 立即执行,不等待请求1
res.json({message: '快速响应'});
});关键概念解释
单线程 vs 多线程处理
传统多线程模型(如Apache):
- 每个请求分配一个线程
- 线程阻塞等待I/O操作
- 内存消耗大,上下文切换开销大
Node.js单线程模型:
- 主线程处理所有请求
- I/O操作异步执行
- 内存消耗小,无上下文切换
线程池的作用
Node.js虽然主线程是单线程,但底层使用了libuv线程池:
- 文件系统操作
- DNS查询
- 某些CPU密集型操作
这些操作在后台线程池中执行,不阻塞主线程。
性能优势
内存使用对比
Apache (多线程):
- 每个线程: ~2MB
- 1000个并发: ~2GB内存
Node.js (单线程):
- 主线程: ~10MB
- 1000个并发: ~50MB内存适用场景
Node.js适合:
- I/O密集型应用(API服务、Web应用)
- 实时应用(聊天、游戏)
- 微服务架构
Node.js不适合:
- CPU密集型计算
- 大量同步操作
测试验证
运行提供的示例代码:
- 启动服务器:
node express-demo.js - 运行并发测试:
node test-concurrent.js
你会看到:
- 快接口立即返回
- 慢接口并发处理
- 总体响应时间优化
总结
Node.js通过事件循环和非阻塞I/O实现了高并发处理能力。虽然是单线程,但通过异步机制避免了阻塞,这就是为什么Node.js能够用较少的资源处理大量并发请求的原因。