最近在做一个反对多过程申请的 Node 服务,要反对多并发申请,而且申请要按先后顺序串联同步执行返回后果。

对,这需要就是这么奇琶,业务场景也是那么奇琶。

需要是实现了,为了对 Node.js 高并发申请原理有更深一些的了解,特意写一篇文章来坚固一下相干的知识点。

问题

Node.js 由这些关键字组成: 事件驱动、非阻塞I/O、高效、轻量

于是在咱们刚接触 Node.js 时,会有所疑难:

  • 为什么在浏览器中运行的 JavaScript 能与操作系统进行如此底层的交互?
  • Node 真的是单线程吗?
  • 如果是单线程,他是如何解决高并发申请的?
  • Node 事件驱动是如何实现的?

下来咱们一起来解秘这是怎么一回事!

架构一览

下面的问题,都挺底层的,所以咱们从 Node.js 自身动手,先来看看 Node.js 的构造。

  • Node.js 规范库,这部分是由 Javascript编写的,即咱们应用过程中间接能调用的 API。在源码中的 lib 目录下能够看到。
  • Node bindings,这一层是 Javascript 与底层 C/C++ 可能沟通的要害,前者通过 bindings 调用后者,相互交换数据。
  • 第三层是撑持 Node.js 运行的要害,由 C/C++ 实现。
  • V8:Google 推出的 Javascript VM,也是 Node.js 为什么应用的是 JavaScript 的要害,它为 JavaScript 提供了在非浏览器端运行的环境,它的高效是 Node.js 之所以高效的起因之一。
  • Libuv:它为 Node.js 提供了跨平台,线程池,事件池,异步 I/O 等能力,是 Node.js 如此弱小的要害。
  • C-ares:提供了异步解决 DNS 相干的能力。
  • http_parser、OpenSSL、zlib 等:提供包含 http 解析、SSL、数据压缩等其余的能力。

单线程、异步

  • 单线程:所有工作须要排队,前一个工作完结,才会执行后一个工作。如果前一个工作耗时很长,后一个工作就不得不始终等着。Node 单线程指的是 Node 在执行程序代码时,主线程是单线程
  • 异步:主线程之外,还保护了一个"事件队列"(Event queue)。当用户的网络申请或者其它的异步操作到来时,Node 都会把它放到 Event Queue 之中,此时并不会立刻执行它,代码也不会被阻塞,持续往下走,直到主线程代码执行结束。

注:

  • JavaScript 是单线程的,Node 自身其实是多线程的,只是 I/O 线程应用的 CPU 比拟少;还有个重要的观点是,除了用户的代码无奈并行执行外,所有的 I/O (磁盘 I/O 和网络 I/O) 则是能够并行起来的。
  • libuv 线程池默认关上 4 个,最多关上 128 个 线程。

事件循环

Nodejs 所谓的单线程,只是主线程是单线程。

  • 主线程运行 V8 和 JavaScript
  • 多个子线程通过 事件循环 被调度

能够形象为:主线程对应于老板,正在工作。一旦发现有工作能够调配给职员(子线程)来做,将会把任务分配给底下的职员来做。同时,老板持续做本人的工作,等到职员(子线程)把工作做完,就会通过事件把后果回调给老板。老板又不停反复解决职员(子线程)子工作的实现状况。

老板(主线程)给职员(子线程)分配任务,当职员(子线程)把工作做完之后,通过事件把后果回调给老板。老板(主线程)解决回调后果,执行相应的 JavaScript。

更具体的解释请看下图:

1、每个 Node.js 过程只有一个主线程在执行程序代码,造成一个执行栈(execution context stack)。

2、Node.js 在主线程里保护了一个"事件队列"(Event queue),当用户的网络申请或者其它的异步操作到来时,Node 都会把它放到 Event Queue之中,此时并不会立刻执行它,代码也不会被阻塞,持续往下走,直到主线程代码执行结束。

3、主线程代码执行结束实现后,而后通过 Event Loop,也就是事件循环机制,查看队列中是否有要解决的事件,这时要分两种状况:如果是非 I/O 工作,就亲自解决,并通过回调函数返回到下层调用;如果是 I/O 工作,就从 线程池 中拿出一个线程来解决这个事件,并指定回调函数,当线程中的 I/O 工作实现当前,就执行指定的回调函数,并把这个实现的事件放到事件队列的尾部,线程归还给线程池,期待事件循环。当主线程再次循环到该事件时,就间接解决并返回给下层调用。 这个过程就叫 事件循环 (Event Loop)

4、期间,主线程一直的查看事件队列中是否有未执行的事件,直到事件队列中所有事件都执行完了,尔后每当有新的事件退出到事件队列中,都会告诉主线程按程序取出交 Event Loop 解决。

优缺点

Nodejs 的长处:I/O 密集型解决是 Nodejs 的强项,因为 Nodejs 的 I/O 申请都是异步的(如:sql 查问申请、文件流操作操作申请、http 申请...)

Nodejs 的毛病:不善于 cpu 密集型的操作(简单的运算、图片的操作)

总结

1、Nodejs 与操作系统交互,咱们在 JavaScript 中调用的办法,最终都会通过 process.binding 传递到 C/C++ 层面,最终由他们来执行真正的操作。Node.js 即这样与操作系统进行互动。

2、Nodejs 所谓的单线程,只是主线程是单线程,所有的网络申请或者异步工作都交给了外部的线程池去实现,自身只负责一直的往返调度,由事件循环不断驱动事件执行。

3、Nodejs 之所以单线程能够解决高并发的起因,得益于 libuv 层的事件循环机制,和底层线程池实现。

4、Event loop 就是主线程从主线程的事件队列外面不停循环的读取事件,驱动了所有的异步回调函数的执行,Event loop 总共 7 个阶段,每个阶段都有一个工作队列,当所有阶段被程序执行一次后,event loop 实现了一个 tick。

参考文章:Nodejs探秘:深刻了解单线程实现高并发原理

串联同步执行并发申请

就像下面说的:Node.js 在主线程里保护了一个"事件队列"(Event queue),当用户的网络申请或者其它的异步操作到来时,Node 都会把它放到 Event Queue之中,此时并不会立刻执行它,代码也不会被阻塞,持续往下走,直到主线程代码执行结束。

所以要串联同步执行并发申请的关键在于保护一个队列,队列的特点是 先进先出,按队列外面的程序执行就能够达到串联同步执行并发申请的目标。

计划

  • 依据每个申请的 uniqueId 变量作为惟一令牌
  • 队列外面保护一个后果数组和一个执行队列,把执行队列实现的 令牌与后果 存储在后果数组外面
  • 依据惟一令牌,始终去获取执行实现的后果,距离 200 毫秒,超时等待时间为 10 分钟
  • 始终期待并获取后果,期待到有后果时,才返回给申请;并依据令牌把后果数组外面相应的项删除

队列

代码:

class Recorder {    private list: any[];    private queueList: any[];    private intervalTimer;    constructor() {        this.list = [];        this.queueList = [];        this.intervalTimer = null;    }    // 依据 id 获取工作后果    public get(id: string) {        let data;        console.log('this.list: ', this.list);        let index;        for (let i = 0; i < this.list.length; i++) {            const item = this.list[i];            if (id === item.id) {                data = item.data;                index = i;                break;            }        }        // 删除获取到后果的项        if (index !== undefined) {            this.list.splice(index, 1);        }        return data;    }    public clear() {        this.list = [];        this.queueList = [];    }    // 增加项    public async addQueue(item: any) {        this.queueList.push(item);    }    public async runQueue() {        clearInterval(this.intervalTimer);        if (!this.queueList.length) {            // console.log('队列执行结束');            return;        }        // 取出队列外面的最初一项        const item = this.queueList.shift();        console.log('item: ', item);        // 执行队列的回调        const data = await item.callback();        console.log('回调执行实现: ', data);        // 把后果放进 后果数组        this.list.push({ id: item.id, data });    }    public interval() {        clearInterval(this.intervalTimer);        this.intervalTimer = setInterval(async () => {            clearInterval(this.intervalTimer);            // 始终执行外面的工作            await this.runQueue();            this.interval();        }, 200);    }}const recorder = new Recorder();recorder.interval();export default recorder;

服务

上面模仿一个申请端口的的 Node 服务。

代码:

const Koa = require('koa')const Router = require('koa-router')const cuid = require('cuid');const bodyParser = require('koa-bodyparser')import recorder from "./libs/recorder";const MAX_WAITING_TIME = 60 * 5; // 最大期待时长// web服务端口const SERVER_PORT: number = 3000;const app = new Koa();app.use(bodyParser());const router = new Router();/** * 程序睡眠 * @param time 毫秒 */const timeSleep = (time: number) => {    return new Promise((resolve) => {        setTimeout(() => {            resolve("");        }, time);    });};/** * 程序睡眠 * @param second 秒 */const sleep = (second: number) => {    return timeSleep(second * 1000);};router.post("/getPort", async (ctx, next) => {    const { num } = ctx.request.body;    const uniqueId = cuid();    console.log('uniqueId: ', uniqueId);    recorder.addQueue({        id: uniqueId,        callback: getPortFun(num)    });    let waitTime = 0;    while (!ctx.body) {        await sleep(0.2);        console.log('1');        const data: any = recorder.get(uniqueId);        if (data) {            ctx.body = {                code: 0,                data: data,                msg: 'success'            };        }        waitTime++;        // 超过最大工夫就返回一个后果        if (waitTime > MAX_WAITING_TIME) {            ctx.body = {};        }    }});// 返回一个函数function getPortFun(num) {    return () => {        return new Promise((resolve) => {            // 模仿异步程序            setTimeout(() => {                console.log(`num${num}: `, num);                resolve(num * num);            }, num * 1000);        });    };}app.use(router.routes()).use(router.allowedMethods());app.listen(SERVER_PORT);

最初

最近状态很差劲,所以最近的原创技术文章有点难产了

心态急需调整,周末想出去玩,放松一下本人,找回那个斗志满满的真我才行,唉。