Node.js 异步原理
写文章写得实在生无可恋,依旧看起了书——《深入浅出 Node.js》,觉得自己确实有错,之后如果有人问我 Node.js 入门怎么入门我可能还是会推荐他网上的教程,但是说到推荐书,这确实是一本很棒的书。
——安利完毕,之后进入正题。
在面试时可能真的会遇到这样的情况,面试官问:请问一下异步是怎么实现的。
之前在听小伙伴说到这个问题的时候一脸黑人问号,毕竟仿佛是一知半解,不得其意,今天总算可以好好聊一聊异步了,也算是一个章节的笔记。不过掌握的还不是很透彻,如果有错误请指出。
为什么会有异步
- 单线程同步模型系统资源利用率低下
- 多线程模型线程切换开销大,多线程编程中的同步问题让人头大
异步让单线程远离阻塞,同时规避了线程切换(恢复现场)的开销,让单一线程在执行 I/O 操作后立即进行其他操作。
异步 I/O 与非阻塞 I/O
尽管异步与非阻塞我们常常一起提及,异步也确实解决了非阻塞的问题,但是在计算机内核的角度,这并不是同一回事。
我们知道,阻塞 I/O 造成了 CPU 等待,浪费了系统资源,而非阻塞 I/O 会在执行完毕后立即返回,如果需要获得数据,需要再次读取。系统通过轮询来获取非阻塞 I/O 的结果,对 CPU 资源又存在浪费。
现在主要的轮询技术有:read
/ select
/ poll
/ epoll
。
其中 epoll
在进入轮询后没有检测到 I/O 事件将会休眠,直到事件发生,利用率较高,但仍然需要花费资源去等待与确认。
我们理想中的异步应该是可以在进行一次 I/O 之后不闲置 CPU,不去关心,直到完成之后执行回调。
现实中也确实有好几种异步方案,在 Node.js 和 Windows 下的 IOCP 中都使用线程池完成异步 I/O。Node.js 中利用 libuv 封装,来判断平台进行兼容。
事件循环
Node.js 中的异步 I/O 当然得提到大名鼎鼎的事件循环了,如果面试只面试到这里,相信每个人都能说出来:
事件循环就是执行一次循环(一个 Tick),就检测是否有事件未处理,如果有,就取出事件及相关回调函数,如果存在关联的回调函数,就执行它们,然后进入下一循环,如果没有,就退出进程。
事件循环就是一个典型的生产者消费者模型,由请求生产,事件循环消费,这个循环由 IOCP / 多线程创建。
首先我们引入请求对象的概念,JavaScript 层传入的参数和方法都被封装在请求对象中,当有可用线程时,我们就会调用对象底层对应的方法。
组装好请求对象,送入线程池等待执行,这就完成了我们的第一步。
执行结束后,将会将结果存储,并且调用方法通知 IOCP 将线程交还给线程池。
之后事件循环观察到执行完的请求,进行处理即可。
整个流程如图(摘自深入浅出 Node.js):
非 I/O 的异步 API
这部分介绍了一下 setTimeout
, process.nextTick
, setImmediate
的异同,可以从这里理解为什么 setTimeout
或者 setInterval
实现计划任务是个不靠谱的行为。
setTimeout
时创建的定时器会被插入到定时器观察者内部的一个红黑树中。每次 Tick 执行时,就会从该红黑树中迭代取出定时器对象,检测是否超时,如果超时就立即执行。
由此我们知道,如果两个定时 1ms 的任务,但是一个任务占用了 4 ms 的时间片,那么下一个任务就定然是不精准的。
行为图(摘自深入浅出 Node.js):
如果你需要立即执行一个任务,就应该使用 process.nextTick()
他可以规定和保证在下一个循环中执行。
此外,Node.js 还有一个 setImmediate()
方法,与 process.nextTick()
的区别是,process.nextTick
的回调函数保存在一个数组中,而 setImmediate
保存在链表中,在同一个 Tick 中会清空数组,但是只执行链表中的一项。此外,process.nextTick
在事件循环的检查中(idle 观察者)高于 setImmediate
(check 观察者)。
植入部分
如果您觉得文章不错,可以通过赞助支持我。
如果您不希望打赏,也可以通过关闭广告屏蔽插件的形式帮助网站运作。
epoll 只试用于套接字(网络),然而文件读写也是IO操作。nodejs(libuv)在*nix上使用的是多线程同步读写的方式(https://github.com/libuv/libuv/blob/v1.x/src/unix/fs.c#L313),在macOS上还加了全局锁(https://github.com/libuv/libuv/blob/v1.x/src/unix/fs.c#L697),并不是特别高效。
linux内核支持原生文件异步IO,叫做aio(http://lse.sourceforge.net/io/aio.html);BSD系(包括macOS)的kqueue支持文件IO;libuv都没有使用
回carter:因为linux下的磁盘io和epoll没办法很好地配合呀,给他设置非阻塞是无效的,所以只能用同步加上线程池的方式。
不仅是磁盘io,还有很多linux系统调用是阻塞的,如果不hook的话,多线程大概是最好的办法了。
写得好详细,给你个♡︎。
现在轮询技术好像没见过read(๑❛ᴗ❛๑)
闲来无事自己也写代码做了一些测试(mac):
1、macOS上aio无法直接通知kqueue,sigev_notify_kqueue和SIGEV_KEVENT本来不存在。所以虽然macOS里有EVFILT_AIO的定义,并无卵用。
2、macOS上虽然有sigev_notify_function和SIGEV_THREAD,但是会导致aio_read失败,提示Resource temporarily unavailable,开线程回调也不能用。
3、所以唯一的选项是抛信号。但捕获到的信号回调函数中,siginfo->si_value.si_val始终为NULL。google后发现macOS不支持:https://stackoverflow.com/questions/5116151/how-to-get-user-data-for-aio-signal-handler-in-mac-os-x?rq=1。所以用户无法直接知道到底是哪个异步IO操作完成了。
结论:至少在macOS里原生aio残废了90%,基本没法用。也无怪libuv自己开线程池模拟异步IO了
google到了一些权威博客,基本都是在喷 *nix 的 aio:http://blog.libtorrent.org/2012/10/asynchronous-disk-io/
参考资料:
1、https://www.freebsd.org/cgi/man.cgi?query=kqueue
2、https://www.freebsd.org/cgi/man.cgi?query=aio_read
3、http://www.manpages.info/macosx/kqueue.2.html
4、http://www.cnblogs.com/luminocean/p/5631336.html
5、https://www.freebsdchina.org/forum/viewtopic.php?t=24193&sid=136d80acfd18bf42fd42fd21dbc53626