从时空维度看 I/O 模型

发布于 2024-09-24 12:48:19 字数 6863 浏览 12 评论 0

在学习了 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; } 

异步、非阻塞

例如,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 并调用回调)
  • 回调的 线程/调用栈 在不同环境下不一样:
    • Unix 的 aio_read() 和 Windows 的 ReadFileEx()系统回调 ,具体 线程/调用栈 不确定
    • Node.js 的 fs.readFile() 由 JavaScript 环境在 主线程回调
    • 用户态 的 I/O 多路复用 在 分派的线程回调 (例如 libevent event_base_dispatch() 调用回调)
  • 本质上 —— 通过 CPS (continuation-passing style) 将 “I/O 结果的处理逻辑” 作为 continuation 传递:

异步、阻塞

例如,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 (如果失败)
  • 如果系统/语言不支持,则无法实现:
  • 本质上 —— 属于 非抢占式/协作式多任务 (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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

浪菊怪哟

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

沧笙踏歌

文章 0 评论 0

山田美奈子

文章 0 评论 0

佚名

文章 0 评论 0

岁月无声

文章 0 评论 0

暗藏城府

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文