关于javascript:解秘-Nodejs-单线程实现高并发请求原理以及串联同步执行并发请求的方案

37次阅读

共计 5014 个字符,预计需要花费 13 分钟才能阅读完成。

最近在做一个反对多过程申请的 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);

最初

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

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

正文完
 0