原文标题:Async/Await


原文链接:https://os.phil-opp.com/async-await/#multitasking

公众号: Rust 碎碎念


翻译 by: Praying

在本文中我们将讨论协作式多任务(cooperative multitasking)和 Rust 中的 async/await 特性。我们会详细了解 async/await 在 Rust 中是如何工作的,包括Future trait 的设计,状态机的转换和pinning。 然后,我们通过创建一个异步键盘任务和一个基本的执行器(executor),为我们的内核添加基本的 async/await 支持。

本文在Github[1]上是公开的。如果你有任何问题,请在 Github 上提 issue。你还可以在底部留下评论,本文完整的源码可以在post-12[2]分支看到。

多任务(Multitasking)

多任务[3]是大多数操作系统的基本特征之一,指能够并发地执行多个任务。例如,你可能在阅读本文的同时还运行着一些其他的程序,比如一个文本编辑器或者终端窗口。即使你只开着一个浏览器窗口,依然还会有各种后台任务在运行,管理着你的桌面窗口,检查更新或者索引文件。

尽管看上去似乎所有的任务是以并行的方式在运行,但实际上 CPU 核心一次只能执行一个任务。为了营造任务并行运行的错觉,操作系统会在活动任务之间快速切换,使每个任务都能向前推进一点儿。因为计算机运行速度很快,所以在绝大多数时候我们都注意不到这些切换。

虽然单核 CPU 一次只能执行单个任务,但是多核 CPU 能够真正以并行的方式执行多任务。例如,一个 8 核心的 CPU 可以同时运行 8 个任务。我们会在以后的文章中介绍如何设置多核 CPU。在本文中,为简单起见,我们主要讨论单核 CPU。(值得注意的是,所有的多核 CPU 都是从一个单独的活动核心开始的,所以我们目前可以把它们视作单核 CPU。)

存在两种形式的多任务:协作式多任务(Cooperative multitasking)要求任务周期性地放弃对 CPU 的控制权从而使得其他任务可以向前推进。抢占式多任务(Preemptive multitasking)利用操作系统功能通过强制暂停任务从而在任意时间点进行任务切换。下面我们将更加详细地讨论这两种形式的多任务并分析它们各自的优缺点。

抢占式多任务(Preemptive Multitasking)

抢占式多任务背后的理念是,操作系统控制了什么时间去切换任务。为此,它利用了每次中断时重新获得 CPU 控制这一事实。这样,只要系统有新的输入,就可以切换任务。例如,在鼠标移动或者网络包到达时它也可以切换任务。操作系统还可以通过配置一个硬件定时器在指定时间后发送中断,来决定一个任务被允许运行的准确时长。

下图解释了在一次硬件中断时的任务切换过程:

在第一行,CPU 正在执行程序(Program)A里的任务(Task)A1。所有其他的任务都是暂停的。在第二行,一个硬件中断抵达 CPU。正如Hardware Interrupts[4]这篇文章所描述的那样,CPU 立即停止了任务A1的执行并跳转到定义在中断向量表( interrupt descriptor table , IDT)中的中断处理程序(interrupt handler)。通过这个中断处理程序,操作系统现在再次控制了 CPU,从而使得它能够切换到任务B1而不是继续执行任务A1

保存状态

因为任务会在任意时刻被中断,而此时它们可能正处于某些计算的中间阶段。为了能够在后面进行恢复,操作系统必须将任务的整个状态进行备份,包括它的调用栈(call stack)[5]以及所有的 CPU 寄存器的值。这个过程被称为上下文切换(context switch)[6]

因为调用栈可能非常大,操作系统通常会为每个任务设置一个单独的调用栈,而不是在每次任务切换时都备份调用栈。这样带有单独调用栈的一个任务被称为[执行线程(thread of execution)](<https://en.wikipedia.org/wiki/Thread_(computing "执行线程(thread of execution)")>)或者短线程(thread for short)。在为每个任务使用一个单独的调用栈之后,在上下文切换时就只需要保存寄存器里的内容(包括程序计数器和栈指针)。这种方式使得上下文切换的开销最小化,这是非常重要的,因为上下文切换每秒会发生 100 次。

讨论

抢占式多任务的主要优势是操作系统可以完全控制一个任务的允许执行时间。这种方式下,它可以保证每个任务都获得一个公平的 CPU 时间片,而不需要依靠任务的协作。这在运行第三方任务或者多个用户共享一个系统时是尤其重要的。

抢占式多任务的缺点在于每个任务都需要自己的栈。相较于共享栈,这会导致每个任务更高的内存使用并且经常会限制系统中任务的数量。另一个缺点是操作系统在每一次任务切换时都必须要保存完整的 CPU 寄存器状态,即使任务可能只使用了寄存器的一小部分。

抢占式多任务和线程是一个操作系统的基础组件,因为它们使得运行不可靠的用户态程序成为可能。我们会在以后的文章中充分地讨论这些概念。但是在本文中,我们将主要讨论协作式多任务,它也为我们的内核提供了有用的功能。

协作式多任务(Cooperative Multitasking)

不同于在任意时刻强制暂停正在运行的任务,协作式多任务让每个任务运行直到它自愿放弃对 CPU 的控制。这使得任务在合适的时间点暂停自身,例如在它需要等待一个 I/O 操作时。

协作式多任务通常被用于编程语言级别,例如以协程(coroutine)[7]或者async/await[8]的形式。它的思想是,程序员或者编译器在程序中插入[yield](<https://en.wikipedia.org/wiki/Yield_(multithreading "yield")>)操作,yield 操作放弃 CPU 的控制并允许其他任务运行。例如,可以在一个复杂的循环每次迭代后插入一个 yield。

常见的是将协作式多任务和异步操作(asynchronous operations)[9]相结合。不同于总是等待一个操作完成并且阻止其他任务这个时间运行,如果操作还没结束,异步操作返回一个“未准备好(not ready)”的状态。在这种情况下,处于等待中的任务可以执行一个 yield 操作让其他任务运行。

保存状态

因为任务定义了它们自身的暂停点,所以它们不需要操作系统来保存它们的状态。它们可以在自己暂停之前,准确保存自己所需的状态以便之后继续执行,这通常会带来更好的性能。例如,刚刚结束一次复杂计算的任务可能只需要备份计算的最后结果,因为它不再需要任何中间过程的结果。

语言支持的协作式多任务实现甚至能够在暂停之前备份调用栈中所需要的部分。例如,Rust 中的 async/await 实现存储了所有的局部变量(local variable),这些变量在一个自动生成的结构体中还会被用到(后面会提到)。通过在暂停之前备份调用栈中的相关部分,所有的任务可以共享一个调用栈
,从而使得每个任务的内存消耗比较小。这也使得在不耗尽内存的情况下创建几乎任意数量的协作式任务成为可能。

讨论

协作式多任务的缺点是,一个非协作式任务有可能无限期运行。因此,一个恶意或者有 bug 的任务可以阻止其他任务运行并且拖慢甚至锁住整个系统。因此,仅当所有的任务已知是都能协作的情况下,协作式多任务才应该被使用。举一个反例,让操作系统依赖于任意用户级程序的协作不是一个好的想法。

尽管如此,协作式多任务的强大性能和内存优势使得它依然成为在程序内使用的好方法,尤其是与异步操作相结合后。因为操作系统内核是一个与异步硬件交互的性能关键型(performance-critical)程序,所以协作式多任务似乎是实现并发的一种好方式。

参考资料

[1]

Github: https://github.com/phil-opp/blog_os

[2]

post-12: https://github.com/phil-opp/blog_os/tree/post-12

[3]

多任务: https://en.wikipedia.org/wiki/Computer_multitasking

[4]

Hardware Interrupts: https://os.phil-opp.com/hardware-interrupts/

[5]

调用栈(call stack): https://en.wikipedia.org/wiki/Call_stack

[6]

上下文切换(context switch): https://en.wikipedia.org/wiki/Context_switch

[7]

协程(coroutine): https://en.wikipedia.org/wiki/Coroutine

[8]

async/await: https://rust-lang.github.io/async-book/01_getting_started/04_async_await_primer.html

[9]

异步操作(asynchronous operations): https://en.wikipedia.org/wiki/Asynchronous_I/O

【译】Async/Await(一)——多任务的更多相关文章

  1. [译]async/await中使用阻塞式代码导致死锁 百万数据排序:优化的选择排序(堆排序)

    [译]async/await中使用阻塞式代码导致死锁 这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Cleary的 ...

  2. [译]async/await中使用阻塞式代码导致死锁

    原文:[译]async/await中使用阻塞式代码导致死锁 这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Clea ...

  3. [译]async/await中阻塞死锁

    这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Cleary的两篇博文中翻译过来. 原文1:Don'tBlock o ...

  4. [译]Async/Await - Best Practices in Asynchronous Programming

    原文 避免async void async void异步方法只有一个目的:使得event handler异步可行,也就是说async void只能用于event handler. async void ...

  5. 【译】Async/Await(二)——Futures

    原文标题:Async/Await 原文链接:https://os.phil-opp.com/async-await/#multitasking 公众号: Rust 碎碎念 翻译 by: Praying ...

  6. 【译】Async/Await(五)—— Executors and Wakers

    原文标题:Async/Await 原文链接:https://os.phil-opp.com/async-await/#multitasking 公众号: Rust 碎碎念 翻译 by: Praying ...

  7. 【译】异步JavaScript的演变史:从回调到Promises再到Async/Await

    我最喜欢的网站之一是BerkshireHathaway.com--它简单,有效,并且自1997年推出以来一直正常运行.更值得注意的是,在过去的20年中,这个网站很有可能从未出现过错误.为什么?因为它都 ...

  8. 【译】Async/Await(三)——Aysnc/Await模式

    原文标题:Async/Await 原文链接:https://os.phil-opp.com/async-await/#multitasking 公众号: Rust 碎碎念 翻译 by: Praying ...

  9. 【译】Async/Await(四)—— Pinning

    原文标题:Async/Await 原文链接:https://os.phil-opp.com/async-await/#multitasking 公众号: Rust 碎碎念 翻译 by: Praying ...

随机推荐

  1. MySQL技术内幕InnoDB存储引擎(五)——索引及其相关算法

    索引概述 索引太多可能会降低运行性能,太少就会影响查询性能. 最开始就要在需要的地方添加索引. 常见的索引: B+树索引 全文索引 哈希索引 B+树索引 B+树 所有的叶子节点存放完整的数据,非叶子节 ...

  2. hashmap底层:jdk1.8前后的改变

    将hashmap和currenthashmap放一块进行比较,是因为二者的结构相差不多,只不过后者是线程安全的. 首先说hashmap,在jdk1.8之前,hashmap的存储结构是数组+链表的形式, ...

  3. Python 表达式 i += x 与 i = i + x 等价吗?

    Python 表达式 i += x 与 i = i + x 等价吗? 看个例子 a = [1, 2, 3] b = a # 写法一 b += [4] # 写法二 # b = b + [4] print ...

  4. C++异常之四 异常类型的生命周期

    异常类型的生命周期 1. throw 基本类型: int.float.char 这三种类型的抛出和函数的返回传值类似,为参数拷贝的值传递. 1 int test_1(int num) throw (i ...

  5. 漫谈 HTTP 性能优化

    本文主要是侧重于 HTTP 的优化,对于 HTTPS 后续文章会讲. 既然要做性能优化,那么,我们就需要知道:什么是性能?它都有哪些指标,又应该如何度量,进而采取哪些手段去优化? "性能&q ...

  6. 实验:非GTID 一主多从变级联架构

  7. 一种简单的吉布斯采样modify中应用

    这是主函数clc; clear all; close all; %% 生成初始序列 sequenceOfLength = 20; sequenceOfPop = 4; sequence = produ ...

  8. [从源码学设计]蚂蚁金服SOFARegistry之Data节点变更

    [从源码学设计]蚂蚁金服SOFARegistry之Data节点变更 目录 [从源码学设计]蚂蚁金服SOFARegistry之Data节点变更 0x00 摘要 0x02 引子 0x03 业务范畴 3.1 ...

  9. idea修改项目名导致无法找到主类

    描述 本地创建项目copy或者是修改项目名和文件夹名称后 启动springboot项目失败 控制台报错 错误无法找到主类 解决办法 1. 求助互联网得知 需要执行 mvn clean install( ...

  10. 高性能、低成本的高防 IP 产品能现实吗?

    DDoS 攻击是网络攻击最常用的方式之一,也是企业发展道路上的阻碍.作为业务发展的巨大隐形"地雷",企业想要自建 DDoS 防御的技术门槛很高,且建设周期不可控.这给予了安全厂商海 ...