【纯干货】Node.js eventloop + 线程池源码分析(建议细看)

前言

Coding 应当是一辈子的事业,而不只仅是 30 岁的青春饭
本文已收录 GitHub https://github.com/ponkans/F2E,里面有一线大厂进阶指南,欢迎 Star,持续更新前端

《大前端进阶 Node.js》系列 异步非阻塞(上)中,怪怪带你们看清了异步非阻塞这个渣女,讲了不少以前你们可能没有想清楚的概念细节。node

这一期,咱们回归 Node 的异步 IO 模型,开始以前,先提出几个问题,本文也将围绕这几个问题展开 xio 习。git

  • 线程池是什么?
  • 线程池如何实现线程复用?
  • eventloop 是什么?
  • Node 如何基于线程池和 eventloop 实现异步非阻塞?

线程池

线程池是个啥

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

咱们先来假设一个场景。面试

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

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

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

燃鹅,由于你推荐的太到位了,大家网站火了,一群富婆争着来注册。这个时候问题也就来了,若是 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~:

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

联系我 / 公众号

微信搜索【接水怪】或扫描下面二维码回复”加群“,我会拉你进技术交流群。讲真的,在这个群,哪怕您不说话,光看聊天记录也是一种成长。(阿里技术专家、敖丙做者、Java3y、蘑菇街资深前端、蚂蚁金服安全专家、各路大牛都在)。

接水怪也会按期原创,按期跟小伙伴进行经验交流或帮忙看简历。加关注,不迷路,有机会一块儿跑个步🏃 ↓↓↓

相关文章
相关标签/搜索