Node.js eventloop + 线程池源码分析(建议精读)

avatar
@https://www.tuya.com/

本文由团队成员宗纬 撰写,已授权涂鸦大前端独家使用,包括但不限于编辑、标注原创等权益。

线程池

线程池是个啥

先开启第一个问题,何为线程池。 一项技术的诞生往往是为了解决某个问题,那线程池也不例外。

我们先来假设一个场景。

假如你在某纪佳缘网上班,你的老板让你开发一个推荐服务,做的事情很简单,当有用户访问你的服务的时候,你需要根据用户的一些特性(比如性别)给他推荐对象。比如怪怪在访问的时候,你就需要推荐 富婆

当你接到这个任务,首先要想到的就是每个用户过来的请求应该是个 独立的线程。因为 用户和用户之间是隔离的,而每个线程做的事情就是一系列推荐操作。

当然,这难不倒优秀的你,你可以在每个用户请求的时候新建一个线程,在请求响应结束后销毁它,一切都很美好。

燃鹅,因为你推荐的太到位了,你们 网站火了,一群富婆争着来注册。这个时候问题也就来了,如果 1000 个富婆同时点击,那意味着 1000 个请求会过来,你是不是就要建立 1000 个线程呢?

在这一系列请求结束后,这些线程是不是又会被销毁?线程创建最直观的开销就是 内存,这样的频繁创建和销毁对性能的影响显而易见,同时这样的设计并不能撑其瞬时 峰值流量

因为这样的设计,富婆们得不到满足,某纪佳缘岌岌可危。

这时,线程池应运而生。之前讲过,一项技术的诞生永远是为了解决某些问题,那线程池解决了什么问题呢?总结下其实就是线程的生命周期管控。下面我们来细细分析。

线程生命周期管控

先看我们的问题,频繁的创建和销毁线程。解决的办法是啥呢?必然是 线程复用

一个线程被创建之后,即使这一次响应结束了,也不让他被回收,下一次请求来的时候依然让他去处理。

这里很关键的点在于如何让一个 线程不被回收

看似很神奇,一个线程执行完了操作还能继续存在?这么持久?做法其实很简单,写一个 死循环 即可。线程一直处于循环中,当有请求来的时候处理请求,当没有的时候就一直等待,等到了再执行处理,处理完再等待,反复横跳,无限循环。

这里引申出了第二个关键点,处于 死循环中的线程怎么知道啥时候有请求要给他处理

这里不同语言实现方式不完全相同,但大同小异,本质上一定是基于阻塞唤醒。当没有任务的时候,所有线程处于阻塞状态,当任务来的时候,空闲线程去竞争这个任务,取到的线程开始执行,未取到的继续阻塞。

这里大家可能网上看过行行色色的线程池解读,但要深刻理解的话,还是要 从源码入手。(在源码面前,一切的花里胡哨都苍白无力)

先给出 libuv threadPool 源码 地址: 源码地址

我们直接截取线程池实现的核心部分,如下图里的注释,基本实现符合预期。

线程池源码1线程池源码1线程池源码2线程池源码2

简单说明下,上面代码里 uv_cond_signal 等同于唤醒阻塞线程,uv_cond_wait 等同于让当前线程进入阻塞状态。

线程池总结

最后总结下就是,线程池利用死循环让线程无法结束,在等待任务期间处于阻塞状态,利用阻塞唤醒来让线程接收任务(本质上阻塞唤醒基于信号量),从而达到线程复用,结束当前任务后进入下一次循环,周而复始。

eventloop

eventloop 是个啥

eventloop 的含义如同其名字一样:事件循环。

说的通俗一点其实就是一个 while(true)循环,循环里面做的事情就是不断的 check 有没有待处理的任务,如果有就处理任务,如果没有就继续下一次循环。

大致流程如下图。

eventloop 的思想很简单,他并不关心你的回调如何实现,IO 操作何时结束。

他做的事情就是不断的去取事件,取到了就执行。那这里有一个关键的点就是他在哪里取的 event 呢?

答案是: watcher。每个事件循环中都会有观察者,每轮循环都会去观察者中拿事件,然后执行。其实这个所谓的 watcher 就是一个用来存放事件的 queue(队列)。

怎么来理解这个 watcher 呢,深入浅出 nodejs 里面给了一个很形象的比喻。


在餐厅里,前台小妹往往负责记录客人的点菜,厨师在后厨做菜。小妹在拿到客人的菜单后会把菜单放到厨房,而厨师只需要不断的看菜单,做菜,再看菜单,做菜。他并不关心是谁点的菜,也不关心这个菜在什么时候点的。

这里这个放菜单的地方就是那个 watcher,本质上是一个 queue,厨师就如同 eventloop,不断的处于做菜的循环中,每一轮循环会去取 queue 里面的请求,如果有回调就执行回调,没有的话进入下一轮循环。

eventloop + 线程池 = 异步非阻塞

上面比较详细的讲解了线程池和 eventpoll,接下来我们来看一下如何用其来实现异步非阻塞。

我们来一步一步捋清思路。首先,可爱的你发起了一个 IO 调用,从 《大前端进阶 Node.js》系列 异步非阻塞 中讲过,一个 IO 调用要么是阻塞调用,要么是先非阻塞的发起 IO,再在需要看结果的时候阻塞的去获取,显然这两种模式都不是我们想要的。

我们要的是异步非阻塞,所以这个 IO 调用一定不是在主线程中执行,这个时候我们就能联想到上面的线程池。

主线程不能被阻塞,但线程池里面的线程可以,主线程只需要把 IO 调用交给线程池来执行,自己就可以愉快的玩耍,以此达到了我们的第一个目标: 非阻塞

异步 呢?如何让线程池里面的调用在结束的时候去执行回调?这个时候 eventloop 闪亮登场。

在线程池 IO 处理结束后,会主动的把结束的请求放入 eventloop 的观察者(watcher)中,也就是我们的 queue 中,eventloop 处于不断循环的状态,当下一次循环 check 到 queue 里有请求的时候,就会取出来然后执行回调,这样我们想要的 异步 就达到了。

最终通过线程池和 eventloop 结合,呈现出的效果就是,当你发起一次 IO 调用,你无需阻塞的等待 IO 结束,也无需在想利用 IO 结果的时候不断的轮询,整个 IO 过程 对主线程而言非阻塞,并且自动结束时执行回调,达到我们想要的异步非阻塞。

最后,我们引用一张《深入浅出 Nodejs》里的图。

异步非阻塞异步非阻塞

如上图,在发起异步调用后,会封装一个请求参数,里面会包括参数和结束时要执行的回调。

这个 request(请求参数) 封装好后会扔给线程池执行,线程池里面的线程如果有空闲,就会在线程池的 queue 中去取这个 request 并执行 IO 操作。

在执行结束之后通知 IOCP,其实就是把这个 requeat 放入一个 queue,这个队列就是线程池和事件循环之间的枢纽。

事件循环在循环的时候发现队列里面有请求,就会取出来并执行相应的回调,一次完美的异步非阻塞就此完成。

总结

本文已收录 Github https://github.com/ponkans/F2E,里面有一线大厂进阶指南,欢迎 Star,持续更新

仔细的看完怪怪的 Node 异步非阻塞(上)(下)两个系列,还不能吊打面试题你尽管来找我~

libuv threadPool 源码 都带你看过了,还不明白,就真的说不过去了!!

最后,精辟的完美总结如下。


核心总结:Node 利用线程池来执行 IO 调用,避免阻塞主线程,执行结束后把结果和请求放入一个队列,利用事件循环来取出队列的请求,最后执行回调,达到了异步非阻塞的效果。

作者更多原创热文传送门,biubiu~:

喜欢的小伙伴加个关注,点个赞哦,感恩💕😊