写在后面
咱们先来看几个常见的说法
- nodejs是单线程 + 非阻塞I/O模型
- nodejs适宜高并发
- nodejs适宜I/O密集型利用,不适宜CPU密集型利用
在具体分析这几个说法是不是、为什么之前,咱们先来做一些筹备工作
从头聊起
一个常见web利用会做哪些事件
- 运算(执行业务逻辑、数学运算、函数调用等。次要工作在CPU进行)
- I/O(如读写文件、读写数据库、读写网络申请等。次要工作在各种I/O设施,如磁盘、网卡等)
一个典型的传统web利用实现
- 多过程,一个申请fork一个(子)过程 + 阻塞I/O(即blocking I/O或BIO)
- 多线程,一个申请创立一个线程 + 阻塞I/O
多过程web利用示例伪代码
listenFd = new Socket(); // 创立监听socket
Bind(listenFd, 80); // 绑定端口
Listen(listenFd); // 开始监听
for ( ; ; ) {
// 接管客户端申请,通过新的socket建设连贯
connFd = Accept(listenFd);
// fork子过程
if ((pid = Fork()) === 0) {
// 子过程中
// BIO读取网络申请数据,阻塞,产生过程调度
request = connFd.read();
// BIO读取本地文件,阻塞,产生过程调度
content = ReadFile('test.txt');
// 将文件内容写入响应
Response.write(content);
}
}
多线程利用实际上和多过程相似,只不过将一个申请调配一个过程换成了一个申请调配一个线程。线程比照过程更轻量,在系统资源占用上更少,上下文切换(ps:所谓上下文切换,略微解释一下:单核心CPU的状况下同一时间只能执行一个过程或线程中的工作,而为了宏观上的并行,则须要在多个过程或线程之间按工夫片来回切换以保障各进、线程都有机会被执行)的开销也更小;同时线程间更容易共享内存,便于开发
上文中提到了web利用的两个外围要点,一个是进(线)程模型,一个是I/O模型。那阻塞I/O到底是什么?又有哪些其余的I/O模型呢?别着急,首先咱们看一下什么是阻塞
什么是阻塞?什么是阻塞I/O?
简而言之,阻塞是指函数调用返回之前,当后退(线)程会被挂起,进入期待状态,在这个状态下,当后退(线)程暂停运行,引起CPU的进(线)程调度。函数只有在外部工作全副执行实现后才会返回给调用者
所以阻塞I/O是,应用程序通过API调用I/O操作后,当后退(线)程将会进入期待状态,代码无奈持续往下执行,这时CPU能够进行进(线)程调度,即切换到其余可执行的进(线)程继续执行,当后退(线)程在底层I/O申请解决完后才会返回并能够继续执行
多进(线)程 + 阻塞I/O模型有什么问题?
在理解了什么是阻塞和阻塞I/O后,咱们来剖析一下传统web利用多进(线)程 + 阻塞I/O模型有什么弊病。
因为一个申请须要调配一个进(线)程,这样的零碎在并发量大时须要保护大量进(线)程,且须要进行大量的上下文切换,这都须要大量的CPU、内存等系统资源撑持,所以在高并发申请进来时CPU和内存开销会急剧回升,可能会迅速拖垮整个零碎导致服务不可用
nodejs利用实现
接下来咱们看看nodejs利用是如何实现的。
- 事件驱动,单线程(主线程)
- 非阻塞I/O
在官网上能够看到,nodejs最次要的两大特点,一个是单线程事件驱动,一个是“非阻塞”I/O模型。单线程 + 事件驱动比拟好了解,前端同学应该都很相熟js的单线程和事件循环这套机制了,那咱们次要来钻研一下这个“非阻塞I/O”是怎么一回事。首先来看一段nodejs服务端利用常见的代码,
const net = require('net');
const server = net.createServer();
const fs = require('fs');
server.listen(80); // 监听端口
// 监听事件建设连贯
server.on('connection', (socket) => {
// 监听事件读取申请数据
socket.on('data', (data) => {
// 异步读取本地文件
fs.readFile('test.txt', (err, data) => {
// 将读取的内容写入响应
socket.write(data);
socket.end();
})
});
});
能够看到在nodejs中,咱们能够以异步的形式去进行I/O操作,通过API调用I/O操作后会马上返回,紧接着就能够继续执行其余代码逻辑,那为什么nodejs中的I/O是“非阻塞”的呢?答复这个问题之前咱们再做一些筹备工作,参考nodejs进阶视频解说:进入学习
read操作根本步骤
首先看下一个read操作须要经验哪些步骤
- 用户程序调用I/O操作API,外部收回零碎调用,过程从用户态转到内核态
- 零碎收回I/O申请,期待数据筹备好(如网络I/O,期待数据从网络中达到socket;期待零碎从磁盘上读取数据等)
- 数据筹备好后,复制到内核缓冲区
- 从内核空间复制到用户空间,用户程序拿到数据
接下来咱们看一下操作系统中有哪些I/O模型
几种I/O模型
阻塞式I/O
非阻塞式I/O
I/O多路复用(过程可同时监听多个I/O设施就绪)
信号驱动I/O
异步I/O
那么nodejs里到底应用了哪种I/O模型呢?是上图中的“非阻塞I/O”吗?别着急,先接着往下看,咱们来理解下nodejs的体系结构
nodejs体系结构,线程、I/O模型剖析
最下面一层是就是咱们编写nodejs利用代码时能够应用的API库,上面一层则是用来买通nodejs和它所依赖的底层库的一个中间层,比方实现让js代码能够调用底层的c代码库。来到最上面一层,能够看到前端同学相熟的V8,还有其余一些底层依赖。留神,这里有一个叫libuv的库,它是干什么的呢?从图中也能看出,libuv帮忙nodejs实现了底层的线程池、异步I/O等性能。libuv实际上是一个跨平台的c语言库,它在windows、linux等不同平台下会调用不同的实现。我这里次要剖析linux下libuv的实现,因为咱们的利用大部分时候还是运行在linux环境下的,且平台间的差异性并不会影响咱们对nodejs原理的剖析和了解。好了,对于nodejs在linux下的I/O模型来说,libuv实际上提供了两种不同场景下的不同实现,解决网络I/O次要由epoll函数实现(其实就是I/O多路复用,在后面的图中应用的是select函数来实现I/O多路复用,而epoll能够了解为select函数的升级版,这个临时不做具体分析),而解决文件I/O则由多线程(线程池) + 阻塞I/O模仿异步I/O实现
上面是一段我写的nodejs底层实现的伪代码帮忙大家了解
listenFd = new Socket(); // 创立监听socket
Bind(listenFd, 80); // 绑定端口
Listen(listenFd); // 开始监听
for ( ; ; ) {
// 阻塞在epoll函数上,期待网络数据筹备好
// epoll可同时监听listenFd以及多个客户端连贯上是否有数据准备就绪
// clients示意以后所有客户端连贯,curFd示意epoll函数最终拿到的一个就绪的连贯
curFd = Epoll(listenFd, clients);
if (curFd === listenFd) {
// 监听套接字收到新的客户端连贯,创立套接字
int connFd = Accept(listenFd);
// 将新建的连贯增加到epoll监听的list
clients.push(connFd);
}
else {
// 某个客户端连贯数据就绪,读取申请数据
request = curFd.read();
// 这里拿到申请数据后能够收回data事件进入nodejs的事件循环
...
}
}
// 读取本地文件时,libuv用多线程(线程池) + BIO模仿异步I/O
ThreadPool.run((callback) => {
// 在线程里用BIO读取文件
String content = Read('text.txt');
// 收回事件调用nodejs提供的callback
});
通过I/O多路复用 + 多线程模仿的异步I/O配合事件循环机制,nodejs就实现了单线程解决并发申请并且不会阻塞。所以回到之前所说的“非阻塞I/O”模型,实际上nodejs并没有间接应用通常定义上的非阻塞I/O模型,而是I/O多路复用模型 + 多线程BIO。我认为“非阻塞I/O”其实更多是对nodejs编程人员来说的一种形容,从编码方式和代码执行程序上来讲,nodejs的I/O调用确实是“非阻塞”的
总结
至此咱们应该能够理解到,nodejs的I/O模型其实次要是由I/O多路复用和多线程下的阻塞I/O两种形式一起组成的,而应答高并发申请时发挥作用的次要就是I/O多路复用。好了,那最初咱们来总结一下nodejs线程模型和I/O模型比照传统web利用多进(线)程 + 阻塞I/O模型的劣势和劣势
- nodejs利用单线程模型省去了系统维护和切换多进(线)程的开销,同时多路复用的I/O模型能够让nodejs的单线程不会阻塞在某一个连贯上。在高并发场景下,nodejs利用只须要创立和治理多个客户端连贯对应的socket描述符而不须要创立对应的过程或线程,零碎开销上大大减少,所以能同时解决更多的客户端连贯
- nodejs并不能晋升底层真正I/O操作的效率。如果底层I/O成为零碎的性能瓶颈,nodejs仍然无奈解决,即nodejs能够接管高并发申请,但如果须要解决大量慢I/O操作(比方读写磁盘),仍可能造成系统资源过载。所以高并发并不能简略的通过单线程 + 非阻塞I/O模型来解决
- CPU密集型利用可能会让nodejs的单线程模型成为性能瓶颈
- nodejs适宜高并发解决大量业务逻辑或快I/O(比方读写内存)
发表回复