像我五岁一样解释非阻塞 I/O 简介您自己的表工厂非阻塞 I/O 实现的好处最终想法

2025-06-08

像我五岁一样解释非阻塞 I/O

介绍

您自己的桌子工厂

非阻塞 I/O 的好处

实现

最后的想法

介绍

十年前,网络应用开发领域发生了重大转变。2009年,Ryan Dahl 发明了 Node.js,因为他对当时流行的 Apache HTTP 服务器处理数千个并发连接的能力有限感到不满。Node.js 项目结合了 JavaScript 引擎、事件循环和 I/O 层。它通常被称为非阻塞 Web 服务器。

非阻塞 I/O 与事件循环相结合的想法并不新鲜。Java 社区早在 2002 年就将NIO模块添加到了 J2SE 1.4 中。Netty一个用于开发 Java 网络应用程序的非阻塞 I/O 客户端-服务器框架,自 2004 年以来一直在积极开发中。早在 Netty 之前,操作系统就提供了在套接字可读或可写时立即收到通知的功能。

如今,你经常会听到或读到诸如“X 是一个非阻塞、事件驱动、可扩展的(此处插入另一个流行词)框架”之类的评论。但这究竟意味着什么?它为什么有用?本文的其余部分结构如下。下一节将通过一个简单的类比来解释非阻塞 I/O 的概念。之后,我们将讨论非阻塞 I/O 的优缺点。下一节将让我们大致了解不同操作系统中非阻塞 I/O 的实现方式。最后,我们将总结一下本文的思考。

您自己的桌子工厂

您的第一位员工和工作台

假设你正在创办一家生产桌子的公司。你租了一栋小楼,只买了一张工作台,因为你只有一名员工,我们姑且叫他乔治。早上,乔治走进大楼,走到工作台前,从收件箱里取了一份新订单。

桌子的尺寸和颜色各不相同。相应的资源和用品可在储藏室获取。然而,有时储藏室缺少所需的材料,例如缺少某种颜色,乔治就需要订购新的用品。乔治喜欢先做完一件事再开始另一件事,所以他会在工作台旁等待,直到新的用品送到。

在这个比喻中,工厂代表一个计算机系统,工作台代表你的 CPU,而 George 是一个工作线程。订购新耗材相当于 I/O 操作,而你可以看作是操作系统,负责协调所有交互。CPU 不具备多任务处理能力,每个操作不仅会阻塞一个线程,还会阻塞整个 CPU,进而阻塞整个计算机。

多名员工,单一工作台

你想着,能不能说服乔治在物资配送期间做点别的事,提高工作效率。新一批货物可能要过好几天才能到,而乔治只会站在那里无所事事。你跟他讲了你的新计划,但他却回答说:“老板,我真的很不擅长环境切换。不过我很乐意回家什么也不做,这样至少不会占着工作台!”

你意识到这并非你所期望的,但至少你可以雇佣另一名员工在乔治在家等待送货时在工作台上工作。你雇佣了吉娜,她在乔治在家时组装了另一张桌子。有时乔治必须等吉娜完成一张桌子后才能继续工作,但尽管如此,生产力几乎翻了一番,因为乔治的等待时间得到了更好的利用。

通过让多名员工共享同一个工作台,我们引入了一种多任务处理方式。多任务处理技术有很多种,这里我们介绍一种非常基础的技术:当一个线程因等待 I/O 而阻塞时,它可以被暂停,让另一个线程使用 CPU。然而,在 I/O 密集型应用中,这种方法需要我们雇佣更多等待的员工(生成更多线程)。雇佣员工的成本很高。还有其他方法可以提高生产力吗?

多任务、无阻塞员工

第二周,吉娜的用品也用完了。她意识到,在等快递的时候,在另一张桌子上工作其实也没什么坏处,于是她请你在快递到货时给她发个短信,这样她就可以在完成当前工作或等待下一张快递时,继续在那张桌子上工作。

现在,吉娜从早上 9 点到下午 5 点都在使用工作台,乔治意识到她比他效率高得多。他决定换工作,但幸运的是,吉娜有一位和她一样灵活的朋友,而且多亏了您卖出的所有桌子,您才买得起第二张工作台。现在,每个工作台上都有一名员工全天工作,利用等待供货的时间处理其他订单。由于您提供了到货通知,他们可以专注于自己的工作,而不必定期查看发货状态。

将工作模式改为在等待送货时不再空闲后,您的员工将以非阻塞方式执行 I/O。虽然 George 在家等待送货后也不再阻塞 CPU,但他仍然在等待,因此处于阻塞状态。Gina 和她的朋友只是在做其他事情,暂停了需要送货的桌子的组装,等待操作系统发出 I/O 结果已就绪的信号。

非阻塞 I/O 的好处

我希望前面的类比能清楚地解释非阻塞 I/O 的基本概念。但它什么时候有用呢?通常来说,当你的工作负载严重依赖 I/O 时,它的好处就开始显现。例如,这意味着你的 CPU 会花费大量时间等待网络接口。

在正确的情况下使用非阻塞 I/O 将提升应用程序的吞吐量、延迟和/或响应速度。它还允许您使用单线程,从而有可能摆脱线程间同步及其相关的所有问题。Node.js 是单线程的,但只需几 GB 的内存即可轻松处理数百万个连接。

一个常见的误解是,非阻塞 I/O 意味着快速 I/O。仅仅因为 I/O 没有阻塞线程,并不意味着它的执行速度会更快。通常来说,没有灵丹妙药,只有权衡利弊。TheTechSolo 上有一篇很棒的博客文章,讨论了围绕这个主题的不同概念的优缺点。

实现

非阻塞 I/O 有多种不同的形式和实现。然而,所有主流操作系统都内置了可用于执行非阻塞 I/O 的内核函数。epoll它通常用于 Linux 系统,其灵感来源于kqueue一篇研究论文,该论文也适用于基于 BSD 的系统(例如 Mac OS X)。

使用 Java 时,开发者可以依赖 Java NIO。在大多数 JVM 实现中,如果适用,Java NIO 都会使用这些内核函数。然而,在细节方面存在一些微妙之处。由于 Java NIO API 足够通用,可以在所有操作系统上运行,因此它无法利用各个实现所喜欢epollkqueue提供的一些高级功能。它类似于非常基本的轮询语义。

因此,如果您需要一些额外的灵活性或性能,您可能希望直接切换到本机传输。NettyJVM 上最好的网络应用程序框架之一,它既支持 Java NIO 传输,也支持Linux 和 Mac OS X 的本机库。

当然,大多数情况下,你不会直接使用 Java NIO 或 Netty,而是会使用一些 Web 应用程序框架。有些框架允许你对网络层进行某种程度的配置。例如,在Vert.x中,你可以选择是否使用原生传输(如果适用),它提供了

最后的想法

“非阻塞”一词的使用方式和语境多种多样。本文我们重点讨论非阻塞 I/O,它指的是线程不等待 I/O 操作完成。然而,有时人们将 API 称为非阻塞仅仅是因为它们不会阻塞当前线程。但这并不一定意味着它们执行的是非阻塞 I/O。

以 JDBC 为例。JDBC 本质上是阻塞的。然而,市面上有一些JDBC 客户端,它们提供异步 API。它会在等待数据库响应时阻塞你的线程吗?不会!但正如我之前提到的,JDBC 本质上是阻塞的,那么谁在阻塞呢?这里的技巧很简单,就是创建一个第二个线程池,用来接收 JDBC 请求并阻塞主线程。

这有什么用呢?因为它能让你继续做主要工作,例如响应 HTTP 请求。即使不是每个请求都需要 JDBC 连接,你仍然可以在线程池阻塞的情况下用主线程响应这些请求。这很好,但仍然会阻塞 I/O,一旦你的工作被 JDBC 通信所束缚,就会遇到瓶颈。

这个领域非常广阔,还有很多细节需要探索。不过,我相信,只要对阻塞和非阻塞 I/O 有基本的了解,当你遇到性能问题时,就能提出正确的问题。你曾经在应用程序中使用过原生传输吗?你这样做是因为你可以做到,还是因为性能问题?请在评论区留言告诉我!


封面图片由Paul Englefield提供

如果您喜欢这篇文章,您可以在 ko-fi 上支持我

鏂囩珷鏉ユ簮锛�https://dev.to/frosnerd/explain-non-blocking-io-like-im- Five-2a5f
PREV
在 Python 中调用 API 的现代方法 HTTP 库 响应数据验证 API 客户端的创建 调用 API 的良好方法
NEXT
使用 Hooks 开始使用 React 什么是 React?JSX 将组件拆分成多个文件 状态效果 自定义 Hooks 类组件 结论