原文标题: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. NOI2020网上同步赛 游记

    Day1 预计得分:\(32pts\)(我裂开了--) T1 美食家 表示考试的时候想到了关于矩阵快速幂的想法,甚至连分段后怎么处理都想好了,但是没有想到拆点,还有不知道怎么处理重边(这个考虑是多余的 ...

  2. python学习之路 初识xml

    import requests from xml.etree import ElementTree as ET r = requests.get('http://www.webxml.com.cn// ...

  3. Centos7.8安装Redis

    一.安装gcc依赖 由于 redis 是用 C 语言开发,安装之前必先确认是否安装 gcc 环境(gcc -v),如果没有安装,执行以下命令进行安装  [root@localhost local]# ...

  4. Java篇:Docker的介绍安装 和常用命令

    文章目录 为什么 出现docker Docker的简介 容器(Container) 镜像(Image) 仓库(Repository) Docker的安装 查看容器 删除镜像 删除容器 部署应用 以my ...

  5. Dotnet Core多版本API共存的优雅实现

    API升级,新旧版本的API共存,怎么管理呢?   一.前言 最近,单位APP做了升级,同步的,API也做了升级. 升级过程中,出现了一点问题:API升级后,旧API也需要保留,因为有旧的APP还在使 ...

  6. Python--批量修改文件名称

    Dr. he 最近忙着毕业论文的事情,需要将一千多张超声切面图的文件名后缀名去掉,例如:剔除文件名"1802014223FAMFIBAI.jpg"的第11个字符至第18个字符&qu ...

  7. 怎么同步fork原git项目

    如何实现 有时候,我们看到有价值的git项目,通常,我们会选择把原项目fork过来,然后自己去把玩研究.然而,原项目进行了更新,fork过来的代码却还是原来的版本,那有没有什么做法,能同时更新到我自己 ...

  8. 收下这款 Vue 项目模版,它将让你的开发效率在 2021 年提高 50%

    这是什么 vue-automation 是一款开箱即用的 Vue 项目模版,它基于 Vue CLI 4 众所周知,虽然 Vue CLI 提供了脚手架的功能,但由于官方的脚手架过于简单,运用在实际项目开 ...

  9. java键盘输入方法-

    字符串缓冲方式 BufferedReader bf = new BufferedReader(new InputStreamReader(System.in)); String line = null ...

  10. 用漫画的形式展现——URL和HTTP

    http请求内容:请求头:get.post等发送请求(其他:head.put.delete.option) host 地址 user-Agent cookie 通行证 head:与get请求类似,不同 ...