从时空维度看 I/O 模型
在学习了 V8 JavaScript 引擎 后,我发现:JavaScript 代码不知道外部 C++ 代码的存在,而 C++ 代码可以灵活的控制多个 JavaScript 代码环境。
地球是圆的,还是平的? —— 看问题的维度(不仅是角度)不同,对世界的认识也不一样。
本文用简单的例子阐述 同步异步 、 阻塞非阻塞 的 不同世界观 。
同步、阻塞
例如,调用 UNIX 系统的 send()
通过 普通的 fd
发送数据:
ssize_t ret = send(fd, buffer, len, flags);
- 当前线程的函数调用 阻塞到 I/O 完成时
同步、非阻塞
例如,调用 UNIX 系统的 send()
通过 非阻塞的 fd
发送数据:
evutil_make_socket_nonblocking(fd); while (len) { ssize_t ret = send(fd, buffer, len, flags); if (ret >= 0) { len -= ret; continue; } if (EVUTIL_SOCKET_ERROR() == EAGAIN) continue; }
- 可以使用 libevent 提供的
evutil_make_socket_nonblocking()
将 fd 设置为非阻塞 - 函数调用 立即返回 :
- 如果可以发送数据,则立即发送数据
- 如果暂时无法发送数据,
EVUTIL_SOCKET_ERROR()
返回EAGAIN
/EWOULDBLOCK
- 否则,socket 错误(比如断开、异常)
异步、非阻塞
例如,Node.js 通过 fs.readFile()
读取文件:
fs.readFile(filename, (err, data) => { if (err) { } }); console.log('start file I/O, and continue');
- 需要系统/语言支持,一般提供基于 回调 (callback) 的接口:
- 函数
fs.readFile()
发起 I/O 请求 ,然后 立即返回 - 在 “发起 I/O 请求” 到 “I/O 完成” 之间,当前线程会 往下执行
console.log()
的代码 - I/O 完成时 ,通过 回调
(err, data) => { ... }
传入数据data
(如果成功)或错误err
(如果失败)
- 函数
- 如果系统/语言不支持,则可以在 用户态 通过 I/O 多路复用 (I/O multiplexing) 模拟 “异步”:
- 例如 libevent 封装了
epoll()
的轮询操作,提供了基于回调的接口 - 但本质上还是 同步 的(主线程 同步处理所有 I/O 并调用回调)
- 例如 libevent 封装了
- 回调的 线程/调用栈 在不同环境下不一样:
- Unix 的
aio_read()
和 Windows 的ReadFileEx()
由 系统回调 ,具体 线程/调用栈 不确定 - Node.js 的
fs.readFile()
由 JavaScript 环境在 主线程回调 - 用户态 的 I/O 多路复用 在 分派的线程回调 (例如 libevent
event_base_dispatch()
调用回调)
- Unix 的
- 本质上 —— 通过 CPS (continuation-passing style) 将 “I/O 结果的处理逻辑” 作为 continuation 传递:
- 如果需要进行 连续多次 I/O 操作,回调函数嵌套 会导致 回调地狱 (callback hell) 问题
- 但可以通过 链式传递 continuation 化简(参考: Chained Promises (JavaScript) )
异步、阻塞
例如,Node.js 用 util.promisify
封装 fs.readFile()
接口:
const readFileAsync = util.promisify(fs.readFile); try { const data = await readFileAsync(filename); } catch (err) { }
- 需要系统/语言支持,一般采用基于 协程 (coroutine)
async/await
的接口:- 函数
readFileAsync
发起 I/O 请求 ,然后 阻塞到 I/O 完成时 - 在 “发起 I/O 请求” 到 “I/O 完成” 之间,当前线程会 切换执行其他代码
- I/O 完成时 ,当前线程 切换回去 ,并返回数据
data
(如果成功)或抛出异常err
(如果失败)
- 函数
- 如果系统/语言不支持,则无法实现:
- 例如 UNIX 系统/C 语言 不支持 协程(参考: Asynchronous I/O Forms )
- 本质上 —— 属于 非抢占式/协作式多任务 (nonpreemptive/cooperative multitasking) 模型;协程调度( 异步、阻塞 )相对于 线程调度( 同步、阻塞 )的优势在于:
- 更简单 —— 没有多线程的 数据竞争 问题,不需要考虑 线程同步问题
- 开销小 —— 无需 系统调用 ,自己管理调用栈内存,没有数量限制
- 更高效 —— 有更多机会被执行(不管怎么切换,执行的代码都在 当前线程 )
世界观
阻塞/非阻塞 像是 空间 维度的对比 —— “发起 I/O 请求” 是否通过 函数返回值 传递 I/O 结果:
阻塞模型 | 非阻塞模型 | |
---|---|---|
I/O 请求调用 何时返回 | I/O 完成时返回 | 立即返回 |
何处处理 I/O 结果 | 调用返回后 | 轮询结束后 或 回调函数里 |
代码(空间)连续性 | 连续 | 非连续 |
代码可读性 | 逻辑连贯 | 逻辑分散 |
同步/异步 像是 时间 维度的对比:
同步模型 | 异步模型 | |
---|---|---|
在执行 I/O 期间 | 只等待 I/O 完成 | 会执行其他代码 |
执行(时间)连续性 | 连续 | 非连续 |
代码执行效率 | 线程利用率低 | 线程利用率高 |
模型对比
- 对于 同步、阻塞模型 ,常用 多进程/多线程 提高 I/O 吞吐量(多个进程/线程 同时发起 I/O,分别等待 各自 I/O 结果)
- 对于 同步、非阻塞模型 ,常用 I/O 多路复用 提高 I/O 吞吐量(一个线程 同时发起 多个 I/O,同时轮询 所有 I/O 结果)
- 对于 异步模型 ,由于 回调/协程 调度顺序不确定 ,需要在 I/O 完成后检查 上下文 (context) 的 有效性
- proactor 模式 被认为是 reactor 模式 (I/O 多路复用, 同步模型 )的 异步模型 变体(TBD:不要太纠结)
- future-promise 模型 可以认为是 非阻塞(不阻塞发起 I/O 请求)+ 阻塞(阻塞等待 I/O 完成) 的模型(同步或异步 取决于 最后阻塞等待的实现方式)
写在最后
随着编程语言的发展,I/O 模型不断优化:
- 效率优化 —— 从 同步 到 异步
- 可读性优化 —— 从 非阻塞 到 阻塞
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 单 Epoll 多线程 IO 模型
下一篇: 不要相信一个熬夜的人说的每一句话
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论