正如你所看到的,涉及的线程不止一个。我不是说这听起来像是一个启示,但是解释并发是如何完成的是一个必要的改进。但是,这是一个很大的,但是,不像.NET,你没有选择,只能写你的代码异步。正如你在上面的代码示例中看到的,你不能在Node中编写同步代码。事件触发后,每个事件的工作都会立即由循环委托给事件处理程序。工作由线程池中的工作者线程拾取,然后回调到发送请求的事件循环。这是一种微妙的,但单线程*几乎*从不忙,什么时候它只是在很短的时间内忙碌。
这与我们的模式至少有三种不同的方式:
- aysn。
- 事件循环只是发布和订阅线程池,因此没有上下文切换。
- 事件循环从不阻塞。
一些可能的瓶颈
这里只见到讨论下自己的理解,欢迎指正。
首先,文件的 I/O 方面,用户代码的运行,事件循环的通知等,是通过 Libuv 维护的线程池来进行操作的,它会运行全部的文件系统操作。既然这样,我们抛开硬盘的影响,对于严谨的 C/C++ 来说,这个线程池一定是有大小限制的。官方默认给出的大小是4。当然是可以改变的。在启动时,通过设置UV_THREADPOOL_SIZE
来改变这个值即可。不过,最大也只能是128,因为这个是涉及到内存占用的。
这个线程池对于所有的事件循环是共享的。当一个函数要使用线程池的时候(比如调用uv_queue_work
),Libuv 会预先分配并初始化UV_THREADPOOL_SIZE
所允许的线程出来。而128占用的内存大约是 1MB,如果设置的太高,当使用线程池频繁时,会因为内存占用过多而降低线程的性能。具体说明;
对于网络 I/O 方面,以 Linux 系统下来说,网络 I/O 采用的是 epoll 这个异步模型。它的优点是采用了事件回调的方式,大大降低了文件描述符的创建(Linux下什么都是文件)。
在每次调用epoll_wait
时,实际返回的是就绪描述符的数量,根据这个值,去 epoll 指定的数组里面取对应数量的描述符,是一种内存映射的方式,减少了文件描述符的复制开销。
上面提到的 epoll 指定的数组,它的大小即可监听的数量大小,它在不同的系统下,有不同的默认值,可见这里epoll create。
有了大小的限制,还远不够,为了保证运行的稳定,防止你在调用 epoll 函数时,指针越界,导致内存泄漏。还会用到另外一个值maxevents
,它是epoll_wait
所能处理的最大数量,在调用epoll_wait
时可以指定。一般情况下小于创建时(epoll_create)的数组大小,当然,也可以设置的比 size 大,不过应该没什么用。可以想到如果就绪的事件很多,超过了maxevents
,那么超出的事件就要等待前面的事件处理完成,才可以继续,可能会导致效率的下降。
在这种情况下,你可能会担心事件会丢失。其实,是不会丢失的,它会通过ep_collect_ready_items
将这些事件保存在一个队列中,在下一个epoll_wait
再进行通知
Node.js 不适合做什么
虽然看起来,Node.js 可以做很多事情,并且拥有很高的性能。比如做聊天室,搭建 Blog 等等,这些 I/O 密集型的应用,是比较适合 Node.js 的。
但是,有一种类型的应用,可能 Node.js 处理起来会比较吃力,那就是 CPU 密集型的应用。前文提到,Libuv 通过事件循环来处理异步的事件,这是存在于 Node.js 主线程的机制。通过这个机制,所有的 I/O 操作,底层API的调用都变成了异步的。但用户的 Javascript 代码是运行在主线程中的,如果这部分代码运行耗时很长,就会导致事件循环被阻塞。因为,它对于事件的处理,都是按照队列顺序的,所以如果其中的任何一个事务/事件本身没有完成,那么其他的回调、监听器、超时、nextTick() 都得不到运行的机会,被阻塞的事件循环没有机会去处理它们。这样下去,轻则效率降低,重则运行停滞。
比如我们常见的模板渲染,压缩,解压缩,加/解密等操作,都是 Node.js 的软肋,所以使用的时候要考虑到这方面。