各种类型的IO:阻塞、非阻塞、多路复用和异步

解释阻塞、非阻塞、多路复用和异步IO

我发现对于软件程序员来说很难分清楚各种类型的IO。对于阻塞,非阻塞,多路复用和异步IO有很多的混淆点。所以我想尝试解释清楚各种IO类型意味着什么

硬件层面

在现代操作系统中,IO(输入/输出)是一种和外围设备交换数据的方式。包括读写磁盘或SSD,通过网络发送和接受数据,在显示器上显示,接入键盘和鼠标输入,等等。

现代操作系统和外围设备的交流取决于外围设备的特定类型以及他们的固件版本和硬件能力。通常来说,你可以认为外围设备是很高级的,他们可以同时处理多个并发的读写数据请求。也就是说,串行交流的日子一去不返了。在这些场景中,外围设备和CPU间的交流在硬件层面都是异步的。

这个异步机制被称为硬件中断(hardware interrupt)。想想一个简单的场景,CPU请求外围设备去读取一些数据,接着CPU会进入一个无限循环,每一次都会检查外围设备的数据是否可用,直到获得了数据为止。这种方法被称为轮询(polling),因为CPU需要保持检查外围设备。在现代硬件中,取而代之发生的是CPU请求外围硬件执行操作,然后就忘了这件事,继续处理其他的CPU指令。只要外围设备做完了,他会通过电路中断来通知CPU。这发生在硬件中,CPU因此不需要停下来或者检查这个外围设备,可以继续执行其他的工作,直到周边设备说已经做完了。

在软件层面

现在我们了解了硬件中发生的事,我们可以移动到软件这一侧了。在这一层IO通过多种方式被暴露:阻塞,非阻塞,多路复用和异步。让我们一个个来仔细解释。

阻塞/Blocking

还记得用户程序如何在一个进程内运行,代码是在线程的上下文中执行的吗?你总是会遇到需要编写一个需要从文件中读取数据的程序的情况。使用阻塞IO,你所做的是从你的线程中请求操作系统,将线程置于休眠(sleep),当数据可用于被消费时操作系统会唤醒线程。

也就是说,阻塞IO之所以被称为阻塞是因为使用他的线程会被阻塞直到IO完成。

非阻塞/Non-Blocking

阻塞IO的问题是当你的线程在休眠时,他除了等IO完成不能干其他事。有时候,你的程序可能没有其他事可做了。但如果还有其他事需要做的话,能在等待IO的时候并发做可是极好的。

其中一种实现方式被称为非阻塞IO。\他的思想是当你读取一个文件时,OS只是简单返回给你文件的内容或者一个等待状态告诉你IO还未完成,而不是将线程休眠。他不会阻塞你的线程,但之后检查IO是否完成的工作还是交给了你。这意味着当处于等待状态时,你可以去做一些工作,当你再次需要IO时,可以再读取一次,那时候IO可能已经完成了,文件的内容会返回,如果还是处于等待状态的话,你可以选择继续做其他事。

多路复用/Multiplexed

非阻塞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都完成的时候或其中一个完成的时候再唤醒他。

异步/Async

多路复用IO的问题是,在IO准备好供你处理之前,你仍然在休眠。同样,对于许多程序来说,这很好,也许你在等待IO操作完成的时候没有其他事情可做。但有时,你确实有其他事情可以做。也许你正在计算PI的位数,同时也在对一堆文件的值进行求和。你想做的是将所有文件的读取排成队列,当你等待它们被读取时,你将计算PI的数字。当一个文件读完后,你要总结它的值,然后再去计算更多的PI数字,直到另一个文件读完。

为了实现这个目标,你需要一种方法让你的PI数字计算在完成时被IO打断,并且你需要IO在完成时执行中断。

这是通过事件回调完成的。执行读取的调用需要一个回调,并立即返回。在IO完成的时候,操作系统会暂停你的线程,并执行你的回调。一旦回调执行完毕,它将恢复你的线程。

多线程 vs 单线程?

你会注意到,我所描述的各种IO都只说到一个单线程,也就是你的主应用线程。事实上,IO并不需要一个线程来执行,因为正如我在开始时解释的那样,外围设备都是在自己的电路中异步执行IO的。因此,在一个单一的线程模型中,可以做阻塞、非阻塞、多路复用和异步的IO。这就是为什么并发IO可以在没有多线程支持的情况下工作。

现在,如果你需要,对IO操作的结果所做的处理,或者请求IO操作的处理,显然可以是多线程的。这允许你在并发的IO之上进行并发的计算。所以没有什么能阻止多线程和这些IO机制的混合。

事实上,有一种非常流行的第五种IO,它确实依赖于多线程。它经常被混淆地称为非阻塞式IO或异步式IO,因为它以类似于其中一种的界面出现。事实上,它是在伪造真正的非阻塞或异步IO。它的工作方式很简单,它使用阻塞式IO,但每个阻塞式调用是在它自己的线程中进行的。现在,根据不同的实现,它要么需要一个回调,要么使用一个轮询模型,比如返回一个Future。

结束语

我希望这已经澄清了你对各种类型的IO的理解。重要的是要记住,不是所有的操作系统和所有的外围设备都支持这些。同样,也不是所有的编程语言都为操作系统支持的所有种类的IO提供了API。

就这样吧。所有各种IO的解释。

希望你喜欢!

更多阅读

免责声明

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

内容出处