[Day 18] Node.js 的非同步小实验

前言

有在写 node 的人可能听人提过, node 的底层是一个支援非同步 IO 的 threadpool , 而有读过我前三篇的读者应该可以联想到 epoll 和 IOCP 两种非同步 IO model 的现象, 就是创建一群闲置的 thread , 每当有一个 IO 发生就唤醒一个新的 thread 来运行。如果 threadpool 每个 thread 都在使用中, 就先让 IO 事件排好队等着。我们今天就试着让 node 呈现这种现象。

实验

实验步骤

  1. 创建一个 node 後端 API , 其功能为渲染一个 html 页回传
  2. 写一个前端页面里面包含大量内容, 其会被步骤 1 的 API 渲染
  3. 再写一个 node 应用程序, 可以利用非同步方法同时呼叫第一步的 API
  4. 观察 process 与 thread 的状态

用 express 写了一下 API

import express = require('express');
const router = express.Router();

router.get('/', (req: express.Request, res: express.Response) => {
	// Disable caching for content files
	res.header('Cache-Control', 'no-cache, no-store, must-revalidate');
	res.header('Pragma', 'no-cache');
	res.header('Expires', '0');
	res.render('index', { title: 'Express' });
});

export default router;

再写一个没有功能但有大量内容的前端页面

extends layout

block content
  h1= title
  p Welcome to #{title}
  h1= title
  p Welcome to #{title}
  h1= title
  p Welcome to #{title}
  h1= title
  p Welcome to #{title}
  h1= title
  p Welcome to #{title}
  h1= title
  p Welcome to #{title}
  h1= title
  p Welcome to #{title}
  h1= title
  p Welcome to #{title}
  h1= title
// 往下延伸 5000 行

再写一个 node 应用, 呼叫 API

const fetch = require('node-fetch');

for (let i = 0; i < 50; i++)
	fetch('http://127.0.0.1:1337/', { method: 'GET' }).then((res) => {
		if (res.status == 200) {
			const time = new Date();
			console.log('success : ' + time);
		} else {
			const time = new Date();
			console.log('error : ' + time);
		}
	});

次数设定成同时呼叫 50 次

实验记录

  1. 启动 API server 以及 node 应用, 观察 process

https://ithelp.ithome.com.tw/upload/images/20210918/201311648UY7mVArkq.png

可以看到这两个应用程序的 process 正在运行。
  1. 观察 API server 的 thread 状态

    API 被呼叫前
    https://ithelp.ithome.com.tw/upload/images/20210918/20131164RomsY6qdW0.png

    API 正在运作时

https://ithelp.ithome.com.tw/upload/images/20210918/20131164zc9W8gBwST.png

可以理解为 thread 31612 是 main thread

其他 4 条是帮助他完成任务的 threadpool

而 threadpool 在这里做的就是读取网页页面与进行渲染, main thread 做的是进行 network IO
  1. 观察 call API 的 node 应用

https://ithelp.ithome.com.tw/upload/images/20210918/201311642EPKHOo8Zs.png

可以发现由 main thread 发出 IO 後, 就只有 main Thread 在等待回传, threadpool 基本都在闲置
  1. 给 API 触发时间加上标记, 以及回传时加上闲置时间
router.get(
	'/',
	(req: express.Request, res: express.Response, next: express.NextFunction) => {
		console.log('In');
		next();
	},
	(req: express.Request, res: express.Response) => {
		setTimeout(() => {
			// Disable caching for content files
			console.log(new Date());
			res.header('Cache-Control', 'no-cache, no-store, must-revalidate');
			res.header('Pragma', 'no-cache');
			res.header('Expires', '0');
			res.render('index', { title: 'Express' });
		}, 5000);
	}
);

结果是

  1. node 应用的 mian thread 在第一时间就呼叫了 50 次 API ,
  2. API 也在第一时间收到 50 个 request,
  3. 接着 API 的 main thread 闲置 5 秒 ( TP 当然也是闲置 ) ,
  4. main thread 醒来叫醒 threadpool 帮忙处理要渲染的资料 ( 一个一个处理 )
  5. main thread 一发现有 threadpool 处理好的资料就马上回传

所以总共只有停下来等待了 5 秒钟, 因为 Network IO 会被 main thread 掌管, 其没有数量限制的疯狂切换, 以近乎同时的方法处理所有 IO , 而本地资料抓取与渲染, 会被 TP 一个一个处理, 处理完再交给 main thread 回传。而 TP 的数量是 4 个 thread

这个现象就像我昨天写的 IOCP httpServer , main thread 疯狂的接收 Network IO , 和把 NetWork IO 的事件放入 eventQueue, thread pool 一个一个醒来, 各取一个事件处理与回传。

当然两件事还是有蛮大的差别, 千万别觉得他们差不多。(ex: node 的回传会丢回来给 main thread 传, node 的事件拆分没有那麽粗糙, 所以可以做到更细致的事件切换 ,.......)

小结

实验就到这里, 基本上可以利用 epoll 或 IOCP 这类演算法来解释, 但实际上 node 底层更为复杂, 就让我们继续看下面的附注吧

附注

https://stackoverflow.com/questions/63369876/nodejs-what-is-maximum-thread-that-can-run-same-time-as-thread-pool-size-is-fo

懒人包 :

因为这个现象, 有些人在撰写 micro service 中具有大量 IO 的微服务时, 会试着提高 node 的 thread 数量。但这篇文章说, 网路 IO 都在 main thread 完成, 所以提高 node 的 thread 数量没啥用。

总结

Node 的非同步不是单纯的非同步 IO , 而是所有可以非同步的事件被分成两大类, 一类是只能在 main thread 内进行, 但 main thread 却会在自己一条 thread 内快速切换任务完成所有事情, 举例就是 network IO , 另一种就是会唤醒沉睡的 thread ( in threadpool ) 且要求他们执行 (但其实这类事件不全是 IO , 但为了方便之後还是统一称为 IO ) , 像是本地档案读写。

明天进度

简单看看一个用 JS 撰写的 model 如何调用底层的 C++

明天见 !


<<:  [拯救上班族的 Chrome 扩充套件] 规划架构和使用情境

>>:  JavaScript学习日记 : Day6 - 函数(一)

资安学习路上-学习资源整理

学习资源整理-资安社团 决定好要学习哪个面向,那要去哪里学呢??是不是要花大钱去补习才能学得会呢? ...

110/01 - 什麽!startActivityForResult 被标记弃用?

讲到硬体就会用到权限控制,然後一定会用onActivityResult和startActivityF...

Day27 Plugin 从零开始到上架 - iOS instagram APIs

Modules: struct UserInfoResponse: Decodable { var ...

Day 19 - Socket 连线

Day 19 - Socket 连线 昨天我们讲解了如何让我们能在程序内切换分页,今天我们就换个口味...

Swift 语言和你 SAY HELLO!!

第二十二天 各位点进来的朋友,你们好阿 小的不才只能做这个系列的文章,但还是希望分享给点进来的朋友,...