同事推荐, 感觉写的不错就试着翻译了下.
作者: Rubber Ducking
我发现对于软件程序员来说很难分清楚各种类型的 IO. 对于阻塞, 非阻塞, 多路复用和异步 IO 有很多的混淆点.
所以我想尝试解释清楚各种 IO 类型意味着什么
在硬件层面.
在现代操作系统中, IO(输入 / 输出) 是一种和外围设备交换数据的方式. 包括读写磁盘或 SSD, 通过网络发送和接受数据, 在显示器上显示, 接入键盘和鼠标输入, 等等.
现代操作系统和外围设备的交流取决于外围设备的特定类型以及他们的固件版本和硬件能力.
通常来说, 你可以认为外围设备是很高级的, 他们可以同时处理多个并发的读写数据请求. 也就是说, 串行交流的日子一去不返了.
在这些场景中, 外围设备和 CPU 间的交流在硬件层面都是异步的.
这个异步机制被称为硬件中断.
想想一个简单的场景, CPU 请求外围设备去读取一些数据, 接着 CPU 会进入一个无限循环, 每一次都会检查外围设备的数据是否可用, 直到获得了数据为止.
这种方法被称为拉取 (polling), 因为 CPU 需要保持检查外围设备.
在现代硬件中, 取而代之发生的是 CPU 请求外围硬件执行操作, 然后就忘了这件事, 继续处理其他的 CPU 指令. 只要外围设备做完了, 他会通过电路中断来通知 CPU.
这发生在硬件中, CPU 因此不需要停下来或者检查这个外围设备, 可以继续执行其他的工作, 直到周边设备说已经做完了.
在软件层面
现在我们了解了硬件中发生的事, 我们可以移动到软件这一侧了.
在这一层 IO 通过多种方式被暴露: 阻塞, 非阻塞, 多路复用和异步. 让我们一个个来仔细解释.
阻塞
还记得用户程序如何在一个进程内运行, 代码是在线程的上下文中执行的吗?
你总是会遇到需要编写一个需要从文件中读取数据的程序的情况.
使用阻塞 IO, 你所做的是从你的线程中请求操作系统, 将线程置于休眠 (sleep), 当数据可用于被消费时操作系统会唤醒线程.
也就是说, 阻塞 IO 之所以被称为阻塞是因为使用他的线程会被阻塞直到 IO 完成.
非阻塞
阻塞 IO 的问题是当你的线程在休眠时, 他除了等 IO 完成不能干其他事.
有时候, 你的程序可能没有其他事可做了.
但如果还有其他事需要做的话, 能在等待 IO 的时候并发做可是极好的.
其中一种实现方式被称为非阻塞 IO.
他的思想是当你读取一个文件时, OS 只是简单返回给你文件的内容或者一个等待状态告诉你 IO 还未完成, 而不是将线程休眠.
他不会阻塞你的线程, 但之后检查 IO 是否完成的工作还是交给了你.
这意味着当处于等待状态时, 你可以去做一些工作, 当你再次需要 IO 时, 可以再读取一次, 那时候 IO 可能已经完成了, 文件的内容会返回, 如果还是处于等待状态的话, 你可以选择继续做其他事.
多路复用
非阻塞 IO 的问题是如果你在等待 IO 的过程中要做的其他事情就是另外的 IO 的话, 事情会变得很奇怪.
在一个好的场景下, 你请求 OS 去读取文件 A 的内容, 然后去做一些重计算的工作, 做完之后再去检查文件 A 是否完成读取, 如果完成了, 你再做一些关于这个文件内容的操作, 不然就继续做其他的工作, 循环往复.
但在一个坏的场景中, 你没有重计算的工作要去做, 而是需要去读取另一个文件 B.
那除了等待他们还有什么事要做呢?
没有了, 你的程序就进入了一个死循环, 判断文件 A 是否被读取完毕, 接着再去判断文件 B, 一遍又一遍.
要么你使用简单的状态轮询, 这会导致过多消耗 CPU, 或者你手动加入一些随意的休眠时间, 不过这也意味着你将延迟知道 IO 完成, 这会降低程序的吞吐.
为了避免这个问题, 你可以使用多路复用 IO 来代替.
他所做的是你再次阻塞在 IO 上, 但这次不仅仅是一个一个的 IO 操作, 你可以将所有需要的 IO 操作塞入队列, 阻塞在所有的操作上. 当其中有一个 IO 完成之后 OS 会唤醒你.
一些多路复用的实现提供了更多的控制, 你可以设置在特定一些 IO 操作完成之后再被唤醒, 例如 A 和 C 文件或 B 和 D 文件完成的时候.
所有你可以调用非阻塞读取文件 A, 然后非阻塞读取文件 B, 最后告诉操作系统将我的线程置于休眠, 当 A 和 B 的 IO 都完成的时候或其中一个完成的时候再唤醒他.
异步
多路复用 IO 的问题是在 IO 完毕前你还是处于休眠状态.
又一次, 这对一些程序来说可行, 那些除了等待 IO 操作完成外没有其他操作要去执行的程序.
但有时候, 你确实需要去做其他事情.
可能你正在计算 PI 的数字, 同时也在汇总一些文件的值.
你想要进行的操作是将所有的读操作入队列, 当等待他们读取完成前, 你可以继续计算 PI. 当一个文件读取完成后, 你可以汇总他的值, 然后继续进行 PI 的计算直到另一个文件完成读取.
为了让这可行, 你需要一种方式当 IO 完成时中断 PI 的计算, 并且你需要 IO 来执行这个操作当他完成时.
这通过事件回调完成. 执行读操作的调用会需要一个回调, 并且调用立即返回. 当 IO 完成时, 操作系统会挂起你的线程, 并执行你的回调. 当回调完成时, 他会恢复你的线程.
多线程 vs 单线程?
你可能已经注意到我所描述的所有线程都是关于单个线程的, 也就是你的主线程.
真相是, IO 的执行不依赖于线程, 这我在最开始就已经解释过了, 外围设备都是在他们自己的电路里异步执行 IO.
所以阻塞, 非阻塞, 多路复用和异步 IO 都是可能在单线程模型中被执行的.
这也是为什么并发 IO 可以不借助于多线程支持来工作.
现在, 对于处理 IO 操作完成的结果, 或者请求 IO 操作很明显是可以多线程的, 如果你需要的话. 这允许你在并发 IO 之上执行并发计算. 所以没有什么东西阻止多线程和这些 IO 机制结合.
事实上, 这里也有第五种受欢迎的基于多线程的 IO.
他经常被混淆为非阻塞 IO 或异步 IO, 因为他对外暴露出的是类似他们的接口.
真相是, 他是假装的非阻塞或异步 IO. 他的工作方式很简单, 他使用阻塞 IO, 但是每个阻塞操作都是在他自己的线程中 (注: 多线程环境, 非主线程中).
现在取决于他的实现机制, 他要么接收一个回调, 或者使用一种拉取模型, 比如返回一个 Future 对象.
最后
我希望这篇文章可以帮助你澄清对多种 IO 的理解. 还有很重要的一点需要注意, 他们不是被所有的操作系统和所有的外围设备支持的. 相似的, 不是所有的编程语言都暴露了操作系统支持的所有 IO 类型的 API.
这边请, 所有类型的 IO 都解释了.
希望能对你有所帮助.
更多阅读
- non-blocking IO vs async IO and implementation in Java https://stackoverflow.com/a/50205346/172272
- Asynchronous and non-blocking IO
- Multiplexed I/O http://www.linux-mag.com/id/331/
- Reactor pattern https://en.wikipedia.org/wiki/Reactor_pattern
- Proactor pattern https://en.wikipedia.org/wiki/Proactor_pattern
- There is no thread
- Asynchronous I/O and event notification on Linux http://davmac.org/davpage/linux/async-io.html
- What is the status of POSIX asynchronous I/O (AIO)?
- Kernel Asynchronous I/O (AIO) Support for Linux http://lse.sourceforge.net/io/aio.html
- I/O Completion Ports
免责声明
我不是一个系统层面的程序员, 我也不是一个操作系统提供的所有种类 IO 方面的专家. 这篇文章是我尽可能总结我所知的内容, 更偏向于中间层面的知识. 所以如果你发现有任何问题的话请指正我.
来源: https://www.cnblogs.com/fairjm/p/translate-the-various-kinds-of-io-blocking-non.html