【JS】深入理解Nodejs
Nodejs 是什么
Node.js® 是一个免费、开源、跨平台的 JavaScript 运行时环境
- JS拓展应用领域:创建服务器、命令行工具、GUI软件和脚本
- Nodejs把操作系统的能力暴露给了JS
Nodejs 组成部分
Nodejs 依赖于哪些库?
js
import process from "node:process";
// An object listing the version strings of Node.js and its dependencies
// NodeJS版本和它的依赖
console.log(process.versions);
输出:
bash
$ node versions.js
{
node: '22.10.0',
acorn: '8.12.1',
ada: '2.9.0',
amaro: '0.1.8',
ares: '1.34.2',
brotli: '1.1.0',
cjs_module_lexer: '1.4.1',
cldr: '45.0',
icu: '75.1',
llhttp: '9.2.1', // http 协议解析
modules: '127',
napi: '9', // Native 接口
nbytes: '0.1.1',
ncrypto: '0.0.1', // 加密
nghttp2: '1.63.0', // http2
openssl: '3.4.0',
simdjson: '3.10.0', // json 解析
simdutf: '5.5.0', // utf 编码
sqlite: '3.46.1',
tz: '2024a', // timezone 时间
undici: '6.20.0',
unicode: '15.1', // unicode 编码
uv: '1.49.2', // libuv
uvwasi: '0.0.21',
v8: '12.4.254.21-node.21', // V8 引擎
zlib: '1.2.12' // 压缩
}
核心的依赖库:
- V8:JS V8引擎
- uv:提供OS层面接口、事件循环、线程池任务处理
命名常用到的关键词:
- ng:
Next Generation
简写,下一代 - simd:
Single Instruction/Multiple Data
简写,CPU技术,性能优化
Nodejs 核心
- 阻塞与非阻塞
- JS单线程
- 事件循环Event Loop
- libuv事件循环源代码
- 工作线程池
阻塞与非阻塞
前提:调用API接口有成本;并不像人感知的很快,调用一下就返回了结果;快和慢的标准应该是由CPU来评价,而不是人
- Buffer模块: 对应内存;内存分配、读、写
- 内存分类:栈、堆、内存池
- File System模块:对应文件系统;CPU操作磁盘(物理),转起来,找inode,找内容块区域
- Net模块:对应网络协议;发出来速度还行,什么时候收到响应不确定
- 最早被
异步
:内核去监听事件,然后通知应用(应用不用等)
- 最早被
execute typical instruction | 1/1,000,000,000 sec = 1 nanosec |
---|---|
fetch from L1 cache memory | 0.5 nanosec |
branch misprediction | 5 nanosec |
fetch from L2 cache memory | 7 nanosec |
Mutex lock/unlock | 25 nanosec |
fetch from main memory | 100 nanosec |
send 2K bytes over 1Gbps network | 20,000 nanosec |
read 1MB sequentially from memory | 250,000 nanosec |
fetch from new disk location (seek) | 8,000,000 nanosec |
read 1MB sequentially from disk | 20,000,000 nanosec |
send packet US to Europe and back | 150 milliseconds = 150,000,000 nanosec |
Latency Numbers Every Programmer Should Know
阻塞与非阻塞示例:
js
import fs from 'node:fs';
// 阻塞
const data = fs.readFileSync('/file.md'); // blocks here until file is read
console.log(data);
moreWork(); // will run before console.log
// 非阻塞
fs.readFile('/file.md', (err, data) => {
if (err) throw err;
console.log(data);
});
moreWork(); // will run before console.log
JS单线程
从 setInterval
超时说起:
js
// 100ms interval
setInterval(() => {
// 模拟interval回调处理耗时任务1000ms
let start = Date.now();
while (Date.now() - start < 1000) {}
console.log("interval date:", new Date());
}, 100);
现象100ms的定时器变成了1000ms的定时器,为什么会这样?
事件循环Event Loop
事件循环流程图
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
阶段概述
timers
:此阶段执行由setTimeout()
和安排的回调setInterval()
。pending callbacks
:执行推迟到下一次循环迭代的I/O
回调。idle, prepare
:仅在内部使用。poll
:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有回调,除了关闭回调、由计时器安排的回调等setImmediate()
);节点将在适当的时候在此处阻塞。check
:setImmediate()
此处调用回调。close callbacks
:一些关闭回调,例如socket.on('close', ...)
。
libuv事件循环源代码
代码目录:deps/uv/src/{unix|win}/core.c
c
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
int timeout;
int r;
int can_sleep;
r = uv__loop_alive(loop);
// 省略部分代码...
while (r != 0 && loop->stop_flag == 0) {
can_sleep =
uv__queue_empty(&loop->pending_queue) &&
uv__queue_empty(&loop->idle_handles);
uv__run_pending(loop);
uv__run_idle(loop);
uv__run_prepare(loop);
timeout = 0;
if ((mode == UV_RUN_ONCE && can_sleep) || mode == UV_RUN_DEFAULT)
timeout = uv__backend_timeout(loop);
uv__metrics_inc_loop_count(loop);
uv__io_poll(loop, timeout);
for (r = 0; r < 8 && !uv__queue_empty(&loop->pending_queue); r++)
uv__run_pending(loop);
uv__metrics_update_idle_time(loop);
uv__run_check(loop);
uv__run_closing_handles(loop);
uv__update_time(loop);
uv__run_timers(loop);
r = uv__loop_alive(loop);
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
break;
}
// 省略部分代码...
return r;
}
工作线程池
先看一段简单测试代码:
js
let counter = 0;
while (true) {
// console --> stdout --> write
console.log("counter:", counter++);
}
现象:任务管理器里面多核CPU起飞。多线程来完成任务
Process Explorer
可以用Process Explorer管理器查看 进程的状态,比如线程数
UV_THREADPOOL_SIZE 设置threadpool线程数
bash
$ node --help
# 省略...
Environment variables:
UV_THREADPOOL_SIZE sets the number of threads used in libuv's threadpool
Nodejs线程模型
线程模型:JS单线程 + N个工作线程
- JS事件循环的单线程:
- 事件循环、添加任务、回调(定时器、超时、事件完成的通知)
- 只做
接待类
的简单任务,回调也简单处理 (比喻成公司前台)
- N个工作线程:做具体繁重、耗时任务
不要阻塞JS事件循环
阻塞JS事件循环的单线程带来的问题:
- 定时器失效、timeout超时不准
- 无法
接待
更多客户:新的网络连接、UI交互 - 不要在回调中写耗时任务
Node.js的核心模块阻塞事件循环表:
- Encryption加密模块:
crypto.randomBytes
(同步版本)crypto.randomFillSync
crypto.pbkdf2Sync
- Compression压缩模块:
zlib.inflateSync
zlib.deflateSync
- File system文件系统模块:
Sync
后缀结尾的API
- Child process子进程模块:
child_process.spawnSync
child_process.execSync
child_process.execFileSync
任务切分
求平均数:切分前
js
for (let i = 0; i < n; i++) sum += i;
let avg = sum / n;
console.log('avg: ' + avg);
切分后(不卡JS事件循环的单线程,循环会继续转)
js
function asyncAvg(n, avgCB) {
// Save ongoing sum in JS closure.
let sum = 0;
function help(i, cb) {
sum += i;
if (i == n) {
cb(sum);
return;
}
// "Asynchronous recursion".
// Schedule next operation asynchronously.
setImmediate(help.bind(null, i + 1, cb));
}
// Start the helper, with CB to call avgCB.
help(1, function (sum) {
let avg = sum / n;
avgCB(avg);
});
}
asyncAvg(n, function (avg) {
console.log('avg of 1-n: ' + avg);
});
任务拆分下派
- 通过开发C++ 插件来使用内置的 Node.js 工作池
- 任务分子线程、子进程去跑,然后通知而返回结果
为什么异步
异步是编程语言应对CPU多核(并发)能力的一种方案;历史背景:近些年,单核性能提升变慢,开始通过横向加核来增加CPU性能:
- 语言迭代有快慢(应对底层物理核增加)
- 老语言有历史包袱(或是先以三方库的形式存在)、新语言可以从新设计
- 异步发展:网络(内核监听)--> 文件(线程池)--> 代码块(语言支持:加语法糖)--> 成为语言一部分
不同语言方案分类:
- 手动调度(开线程、线程池、三方库实现协程/异步):C、C++、其它
- 异步:JS、Swift、C#、Rust
- 协程:Go、Java
异步总结:
- 减少开发人员的开发成本(利用CPU多核的能力)
- 不用关心数据竞争、线程同步及其它问题
- 不用关心具体的任务在哪个线程去执行
- 不用自已写任务分配和调度
- 让写异步代码和写同步代码一样简单(范式、执行流程)