日常工作中,大量使用到 Node.js, 之前也看过一些资料,这次打算总结一下,加深印象,写出来后也方便后续查阅。
事件循环
事件循环是 Node.js 最核心的概念,所以理解事件循环如何运作对于写出正确的代码和调试是非常重要的。
在计算机领域中事件循环(event loop),又称为消息分发器(message dispatcher)、消息循环(message loop)、消息泵(message pump)或运行循环(run loop),是一种程序构造或设计模式,负责等待并分发程序中的事件或消息。它的工作方式是向内部或者外部的“事件提供方”发出请求(请求通常会被阻塞,直到有新事件产生),待请求被处理后调用所获得的事件对应的回调函数(即“分发事件”)。
Event Loop 可以简单理解为:
- 所有任务都在主线程上执行,形成一个执行栈(Execution Context Stack)。
- 主线程之外,还存在一个 “任务队列”(Task Queue)。系统把异步任务放到 “任务队列” 之中,然后主线程继续执行后续的任务。
- 一旦 “执行栈” 中的所有任务执行完毕,系统就会读取 “任务队列”。如果这个时候,异步任务已经结束了等待状态,就会从 “任务队列” 进入执行栈,恢复执行。
- 主线程不断重复上面的第三步。
Node.js
我们常说 Node.js 是单线程的,但为何能达到高并发呢?原因就在于底层的 libuv 维护一个 I/O 线程池(即上述的 “任务队列”),结合 Node.js 异步 I/O 的特性,单线程也能达到高并发啦。如下所示:
事件循环的操作的顺序如下所示:
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
每个框将被称为事件循环的阶段(phase)。
每个阶段都有一个FIFO回调队列要执行。而每一个阶段一般来说,当事件循环进入给定的阶段,它将执行特定于该阶段的任何操作,然后在该阶段的队列中执行回调,直到队列已被已耗尽或已执行最大数量的回调。当队列已耗尽或达到回调限制,则事件循环将进入下一阶段,依此类推。
每个阶段的作用:
- timers:执行 setTimeout() 和 setInterval() 中到期的 callback。
- I/O callbacks:上一轮循环中有少数的 I/O callback 会被延迟到这一轮的这一阶段执行。
- idle, prepare:仅内部使用。
- poll:最重要的阶段,执行 I/O callback,在适当的条件下 node 会阻塞在这个阶段。
- check:执行 setImmediate() 的 callback。
- close callbacks:执行 close 事件的 callback,例如 socket.on('close',func)
以上属于 Node.js 的宏任务,至于 process.nextTick 和 Promise 则属于微任务,会在每个宏任务切换间被检查和执行,所以它们往往会比较快被放入主线程栈执行。
Node.js 将异步操作放入线程池执行后,在事件循环中监听,线程池处理完任务后通过Epoll通知事件循环执行事件完成的回调操作。
libuv
libuv 是一个强制使用异步、事件驱动编程风格的库。其核心工作是提供事件循环和基于回调的 I/O 和其他活动通知。libuv 提供了许多核心实用工具,如定时器、非阻塞网络支持、异步文件系统访问和子进程等,以支持开发者进行高效的异步编程。
具有下面这些特征:
- 全特征事件循环, 以epoll、kqueue、IOCP、事件端口为后端
- 异步TCP和UDP套接字
- 异步DNS解析
- 异步文件和文件系统操作
- 文件系统事件
- ANSI转义序列控制的TTY
- IPC经由套接字共享,使用Unix域套接字或命名管道(Windows)
- 子进程
- 线程池
- 信号处理
- 高清晰度时钟
- 线程和同步原语(primitive)
线程池
Node.js 使用少量的线程来处理许多客户端的需求。 在 Node.js 中,有两种类型的线程:一个事件循环(即主循环、主线程、事件线程等),以及 Worker Pool(也称为线程池)中的 k 个 Worker 的池。
Node.js 使用 Worker Pool 来处理“昂贵”的任务。 这包括操作系统不提供非阻塞版本的I/O任务,和CPU任务。
I/O密集型:
- DNS:
dns.lookup()
,dns.lookupService()
. - File System: 除了
fs.FSWatcher()
和那些显式同步的文件系统API之外,所有文件系统API都使用libuv的线程池。
CPU 密集型:
- Crypto:
crypto.pbkdf2()
,crypto.scrypt()
,crypto.randomBytes()
,crypto.randomFill()
,crypto.generateKeyPair()
. - Zlib: 除了那些显式同步的之外,所有的zlib API都使用libuv的线程池。
参考
- https://en.wikipedia.org/wiki/Event_loop
- http://nikhilm.github.io/uvbook/basics.html
- https://github.com/robbie-cao/note/blob/master/eventloop.md
- https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick
- https://nodejs.org/en/docs/guides/dont-block-the-event-loop
- https://wizardforcel.gitbooks.io/node-in-debugging/content/3.6.html
本文由 Chakhsu Lau 创作,采用 知识共享署名4.0 国际许可协议进行许可。
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。