异步 I/O
为什么要异步 I/O?
- 用户体验
- 资源分配
- 单线程串行:阻塞,CPU 利用效率低
- 多线程并行:创建线程和执行期线程上下文切换开销较大;锁、状态同步等问题
- 异步 I/O:利用单线程远离锁、状态同步等问题;同时让单线程远离阻塞
阻塞/非阻塞与同步/异步(怎样理解阻塞非阻塞与同步异步的区别?- 知乎)
操作系统对于 I/O 只有两种方式:阻塞和非阻塞
- 阻塞 I/O 调用之后要等系统内核完成所有操作后,调用才结束
- 非阻塞 I/O 调用之后会立即返回
- OS 将计算机所有输入输出设备抽象为文件,内核在 I/O 操作时通过文件描述符进行管理,文件描述符类似于应用程序与系统内核之间的凭证
- 非阻塞 I/O 返回之后,CPU 可以用来处理其他事务
- 完整 I/O 没有完成,业务层为了得到完整的数据,需要进行轮询(poll、epoll)
理想的非阻塞异步 I/O:在 I/O 完成后通过信号或回调将数据传递给应用程序
现实的异步 I/O:让部分线程进行阻塞 I/O 或非阻塞 I/O + 轮询来完成数据获取,让主线程进行数据处理,通过线程之间的通信传递 I/O 得到的数据,模拟异步 I/O
Node 的异步 I/O
libuv 在 *uix 和 Windows 上都是通过线程池实现的异步 I/O,所以 Node 并不是单线程架构
1lib/fs.js ( fs.open() )2src/node_file.cc ( Open )3deps/uv/unix/fs.c ( uv_fs_open() )
- uv_fs_open 调用过程中会创建一个请求对象,把传入的 callback 放到这个请求对象上
- 组装好请求对象后会放入 I/O 线程池中
- 线程池中的 I/O 完毕后,把结果放到请求对象上,并通过 I/O 观察者(每次 Tick 会检查线程池中是否有执行完的请求)把请求对象放到任务队列中,将其当作事件处理
1┌───────────────────────────┐2┌─>│ timers │ (setTimeout, setInterval)3│ └─────────────┬─────────────┘4│ ┌─────────────┴─────────────┐5│ │ pending callbacks │ (I/O callbacks deferred to the next loop)6│ └─────────────┬─────────────┘7│ ┌─────────────┴─────────────┐8│ │ idle, prepare │ (only use internally, process.nextTick)9│ └─────────────┬─────────────┘ ┌───────────────┐10│ ┌─────────────┴─────────────┐ │ incoming: │11│ │ poll │<─────┤ connections, │ (retrieve new I/O events; execute I/O related callbacks (almost all with the exception of close callbacks, the ones scheduled by timers, and setImmediate()); node will **block** here when appropriate)12│ └─────────────┬─────────────┘ │ data, etc. │13│ ┌─────────────┴─────────────┐ └───────────────┘14│ │ check │ (setImmediate)15│ └─────────────┬─────────────┘16│ ┌─────────────┴─────────────┐17└──┤ close callbacks │ (some close callbacks)18 └───────────────────────────┘
每个阶段都有一个 FIFO 队列来执行回调
1// [uv_run 事件循环源码](https://github.com/nodejs/node/blob/master/deps/uv/src/unix/core.c#L365)2while (r != 0 && loop->stop_flag == 0) {3 uv__update_time(loop);4 uv__run_timers(loop);5 ran_pending = uv__run_pending(loop);6 uv__run_idle(loop);7 uv__run_prepare(loop);89 timeout = 0;10 if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)11 timeout = uv_backend_timeout(loop);1213 uv__io_poll(loop, timeout);14 15 // ...1617 uv__run_check(loop);18 uv__run_closing_handles(loop);19 20 // ...21}
uv__io_poll 之前会调用 uv_backend_timeout 回去 timeout,uv_backend_timeout 中检测 uv__has_active_handles、uv__has_active_reqs、idle_handles、pending_queue、closing_handles 都没有就返回 0,否则 uv__next_timeout 获取 timeout
uv__next_timeout 中获取距离此时此刻最先到期的一个 timer 的时间
- 没有 timer 就返回 -1,表示将在 poll 阶段无限制阻塞,以实现之后有异步 I/O 完成可以立即响应
- 最近计时器时间节点小于等于事件循环开始时间,表明有过期的 timer,则返回 0 不进行阻塞,以尽快进入下一循环执行过期的 timer
- 最近计时器时间节点大于事件循环开始时间,则计算这两个的差值,进行适当时间的阻塞(差值或 INT_MAX),以实现尽可能快的处理异步 I/O 事件
推荐阅读:
process.nextTick 和 setImmediate 区别
nextTick 的回调函数保存在数组上,setImmediate 的回调函数保存在链表上,nextTick 会在每轮循环将数组中所有的回调全部执行完,setImmediate 每轮循环只执行链表中的一个回调函数
nextTick 在 idle 阶段,setImmediate 在 check 阶段,优先级 idle > poll > check
实际上这两个名字换一下更合适,但这是过去遗留问题不能随意改变