写在前面
我们先来看几个常见的说法
- 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请求处理完后才会返回并可以继续执行