共计 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);
最初
最近状态很差劲,所以最近的原创技术文章有点难产了 😥
心态急需调整,周末想出去玩,放松一下本人,找回那个斗志满满的真我才行,唉。