menu:
- Node 为何是单线程
- Node 是如何管理 非阻塞 IO
- EventLoop 执行流程
- Node EventLoop 与 浏览器事件环 区别
Node 为何是单线程
Node 是单线程的主要原因是 “性能/简化并发处理” 具体原因如下
- 通过Libuv实现异步非阻塞I/O: Node 设计上通过 libuv 来管理异步非阻塞I/O
- 降低内存消耗: 每个线程都会消耗 内存
- 避免多线程管理减少复杂性: 多线程需要频繁切换 执行上下文, 在操作系统层面上来看多线程有 线程同步, 数据共享, 竞争条件, 死锁问题 等问题增加了复杂性
上下文切换(Context Switch):
当操作系统从一个线程切换到另一个线程就需要保存当前线程的 状态(即上下文),以便下次切换到该线程保持上一次的状态执行
权衡利弊: 这是 Node 在性能上的取舍 有得必有失, 依附于 事件循环得到了更加优异的I/O密集型任务(文件操作, 数据库, 网络操作), 但在 CPU密集型任务(压缩, 解压, 加密, 解密) 上会有较弱的表现
Node 是如何管理 非阻塞 IO
我们要先知道 异步与同步 阻塞/非阻塞的区别
NodeJS 是通过 libuv 来管理 异步非阻塞 I/O, libuv 是由 C 编写 实现异步 I/O 处理的一个库
NodeJS EventLoop:
- 主线程交给 V8 引擎
- NodeAPI 交给 libuv 来管理
- libuv 是通过 多线程阻塞I/O 来实现 异步非阻塞 I/O
- 异步任务完成后 libuv 将回调放入 事件队列中由 主线程进行处理
EventLoop 执行流程
NodeJS 的 EventLoop 是由 libuv 所管理, libuv 内维护着如下 6 个队列, 其中我们只需要 知道可控的 timers, poll, check, close callbacks 四个 事件队列
timers 主要维护 setInterval, setTimeout
poll 事件触发线程可能会在这里阻塞 (取决于是否有 I/O 或 定时器到达), 当 异步回调时间到达 或 I/O 回调执行时 就会回到 timers 阶段
check 主要是管理 setImmediate
close callbacks 主要是管理 关闭时的回调
┌───────────────────────────┐
┌─>│ timers │ setInterval, setTimeout
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │ 本次没执行完, 下次执行回调(不可控/无需管)
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │ 系统内部用的回调队列(无需管)
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │ 事件触发线程在轮训过程中会在 这里阻塞 (1. 执行异步 I/O 回调)(2. 监控时间到达后回到 timer)
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │ setImmediate 回调在这里
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤