[翻译]各个类型的IO - 阻塞, 非阻塞,多路复用和异步

同事推荐,感觉写的不错就试着翻译了下.
原文链接: https://www.rubberducking.com/2018/05/the-various-kinds-of-io-blocking-non.html
作者: Didier A.

我发现对于软件程序员来说很难分清楚各种类型的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

Asynchronous and non-blocking IO

Multiplexed I/O

Reactor pattern

Proactor pattern

There is no thread

Asynchronous I/O and event notification on linux

What is the status of POSIX asynchronous I/O (AIO)?

Kernel Asynchronous I/O (AIO) Support for Linux

I/O Completion Ports

免责声明

我不是一个系统层面的程序员,我也不是一个操作系统提供的所有种类IO方面的专家.这篇文章是我尽可能总结我所知的内容,更偏向于中间层面的知识.所以如果你发现有任何问题的话请指正我.