如何理解Node.js的单线程?
我们常说, Node.js 是单线程的,这句话对新人有很大的误导作用。首先要明确:Node.js 程序并非「单线程」,证明代码如下:
let end = Date.now() + 1000 * 10 for (; Date.now() < end;) {}
运行之后,到活动监视器中搜索 Node,结果如下:
看到没,一个 Node 程序有 7 个线程。到这里,你可能会很困惑,这究竟是怎么回事?其实正确的说法应该是:
单线程的指的是 JavaScript 的执行是单线程的,但 Javascript 的宿主环境并非单线程。
怎么理解这句话呢?其实当你用 node xxx.js
运行程序的时候,操作系统会启动下面 7 个线程:
- 1 个 Javascript 主线程用于执行用户代码
- 1 个 watchdog 监控线程用于处理调试信息
- 1 个 v8 task scheduler 线程用于调度任务优先级,加速延迟敏感任务执行
- 4 个 v8 线程用来执行代码调优与 GC 等后台任务
所以说,我们的 Node 程序中包含的线程实际上是:JavaScript 的宿主环境需要的线程 + JavaScript 的执行线程。
到这里是不是有种豁然开朗的感觉了?别急,我再加一行代码:
require('fs').readFile(require.main.filename, () => {}) let end = Date.now() + 1000 * 10 for (; Date.now() < end;) {}
这个时候活动监视器的结果如下:
WTF?怎么多了 4 个线程?这是怎么回事?其实原因就出现在第一行代码上:
require('fs').readFile(require.main.filename, () => {})
这行代码是异步操作,而 Node 是「异步非阻塞」的语言,如果还是 7 个线程的话,就意味着 JavaScript 主线程也要执行 I/O 操作,从而造成了阻塞。所以 libuv 创建了线程池,默认情况下线程池里面有 4 个线程,所以我们看到的结果是 11。
也就是说,代码里面那些异步操作,全部由线程池来接管,JavaScript 主线程不参与进去,只是执行同步代码而已。下面是 Node 进程结构图:
如果全部是同步代码,那么只会开启 7 个线程,如果存在异步 I/O 操作,则默认会开启 11 个线程。为什么说是「默认」呢?因为 uv_thread_pool 的容量是可以改的,只要设置环境变量 UV_THREADPOOL_SIZE
即可。例如下面的代码把线程池的容量改成 1:
process.env.UV_THREADPOOL_SIZE = 1 require('fs').readFile(require.main.filename, () => {}) let end = Date.now() + 1000 * 10 for (; Date.now() < end;) {}
这个时候就只有 8 个线程了:
所以,Node.js 程序并非单线程,只不过主线程是单线程的,所有的异步 I/O 操作由 libuv 的线程池中的线程进行处理,然后把运行结果通过回调的方式通知到主线程。
码云笔记 » 如何理解Node.js的单线程?