Async/Await

在这篇文章,我们将探索Rust 中的多任务机制和异步/等待特性。我们详细的剖析异步/等待机制如何工作,包括 Future 的设计,状态机转换和 pinning。我们增加了基础的支持在内核中。通过创建异步键盘任务和基本的执行器。

多任务

多任务 是大多数操作系统的基本特性之一,这是一种允许多任务并行执行的能力。 举例来说,你可能打开了一个程序来看这片文章,比如文字编辑器或者 Terminal 窗口,甚至可能有打开了一个单独的浏览器窗口。各种各样任务在后台运行,这样你才能够管理桌面窗口,检查更新或者索引文件。
任务看起来是并行的,但实际上CPU在同一时刻只允许一个任务在执行。为了让用户产生程序在同时运行的错觉,操作系统必须在不同的后台程序之间快速的切换。
但是,单核 CPU 同时只能运行一个任务,但多核 CPU 可以真正同时运行多个任务。
多任务处理系统有两种常见形式:协作式多任务处理系统需要任务主动放弃 CPU 的控制权,以便其他任务可以进行。抢占式多任务使用操作系统提供的能力去切换,操作系统可以任意的强制停止他们。接下来,我们将详细介绍两者的不通。

抢占式多任务

抢占式多任务的核心观念是由操作系统来控制何时切换任务。正因如此,它充分利用了在每个中断发生时,操作系统都会重新获得 CPU 控制权这个特点。这也使得无论何时,当系统中有新的输入可用时得以切换任务。比如,鼠标移动或者网络包到达。
不仅如此,操作系统也可以在配置硬件系统,令其在达到某个时间后主动发送中断,

在第一行,CPU 正在执行 A程序中的 A1任务。其他任务均被暂停。在第二行中,硬件中断到达 CPU。CPU 立刻停止A1任务,然后跳到在 IDT 中定义的了中断处理器。通过指定的中断处理器,操作系统现在重新获得 CPU 控制权,然后然后操作系统允许CPU切换到 B1 任务继续运行。

状态保存

程序可能被任意终止,无论程序是否进行计算。因此稍后继续恢复运行,操作系统必须保存程序运行的全部状态,包括程序调用栈,和 CPU 寄存器的值。这个过程就称为上下文切换。
由于调用栈可能特别庞大, 操作系统每个任务单独设置了自己的调用栈,避免每次任务切换时,需要保存所有任务的调用栈。通过使用单独的调用栈,在任务切换时,只有寄存器内容需要被保存。这个方式最小化了性能损耗,这是上下文切换能够达到100 times/s 的关键。

讨论

抢占式多任务的最主要的优势在于操作系统可以完全控制任务的运行时间。内核可以保证每一个任务可以获得公平的 CPU 时间,不需要信任其他任务。这对于运行一些多用户或者第三方系统式很有用,因为第三方应用程序可能会占据 CPU 时间不释放。

劣势在于抢占式任务需要每个都有自己的栈。与共享栈对比,导致了更高的内存使用率,而且经常达到任务数的上限。另外一个劣势是,操作系统总是需要保存完整的 CPU 寄存器状态,即便在任务发生切换时,程序只使用了一部分寄存器。

协作式多任务

不同于抢占式任务可以在任意时间点强制停止任务,协作式多任务系统允许每个任务主动放弃 CPU 的控制权。任务可以自由选择可以暂停的时间点,比如当程序需要等待 IO 操作时。

协作式多任务的实现通常是在语言层面,比如 Rust 的async/await。这个功能使得无论是程序员或者编译器在程序中插入 yield,都可以放弃 CPU控制权,允许其他程序运行。比如,在每一轮复杂的循环过后进行 yield。

将多任务系统和异步操作绑定在一起,是非常自然的。不同于等待一个操作完成和阻止其他任务同时运行,异步操作会返回一个还没准备好状态。比如,阻塞任务执行一个 yield 操作让其他任务运行。

状态保存

由于任务可以自定义他们的暂停点,也就不再需要操作系统去保存他们的运行状态。取而代之的是他们只需要精确的存储他们真正需要的状态,这样就会有更好的性能。比如,一个任务再复杂的计算后,也许只需要保存结果。

对于实现而言,协作式任务有语言层面的支持,协作式任务能够备份他们需要的部分调用栈。通过只保存相关部分,所有的任务可以共享一个调用栈,这样可以消耗更小的内存。

讨论

协作式任务的缺点是,一个程序可以不释放CPU,一个恶意程序会阻止其他程序运行,甚至拖慢整个系统。协作式任务必须被所有的任务互相信任。比如,操作系统内核就不能基于用户级程序实现。

然而,强力的性能和内存友好是协作式程序一大有点,尤其是和异步操作绑定时。因此一个操作系统内核搭配支持异步的硬件,协作式任务是一个好的方式去实现并发功能。