Node.js eventloop 线程池入门

4,679 阅读8分钟

前言

Coding 应当是一生的事业,而不仅仅是 30 岁的青春饭
本文已收录 GitHub https://github.com/ponkans/F2E,欢迎 Star,持续更新

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

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

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

线程池

线程池是个啥

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

我们先来假设一个场景。

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

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

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

燃鹅,因为你推荐的太到位了,你们网站火了,一群富婆争着来注册。这个时候问题也就来了,如果 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 利用线程池来执行 IO 调用,避免阻塞主线程,执行结束后把结果和请求放入一个队列,利用事件循环来取出队列的请求,最后执行回调,达到了异步非阻塞的效果。