【译】Async/Await(一)——多任务
原文标题: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)程序,所以协作式多任务似乎是实现并发的一种好方式。

参考资料
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(一)——多任务的更多相关文章
- [译]async/await中使用阻塞式代码导致死锁 百万数据排序:优化的选择排序(堆排序)
[译]async/await中使用阻塞式代码导致死锁 这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Cleary的 ...
- [译]async/await中使用阻塞式代码导致死锁
原文:[译]async/await中使用阻塞式代码导致死锁 这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Clea ...
- [译]async/await中阻塞死锁
这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Cleary的两篇博文中翻译过来. 原文1:Don'tBlock o ...
- [译]Async/Await - Best Practices in Asynchronous Programming
原文 避免async void async void异步方法只有一个目的:使得event handler异步可行,也就是说async void只能用于event handler. async void ...
- 【译】Async/Await(二)——Futures
原文标题:Async/Await 原文链接:https://os.phil-opp.com/async-await/#multitasking 公众号: Rust 碎碎念 翻译 by: Praying ...
- 【译】Async/Await(五)—— Executors and Wakers
原文标题:Async/Await 原文链接:https://os.phil-opp.com/async-await/#multitasking 公众号: Rust 碎碎念 翻译 by: Praying ...
- 【译】异步JavaScript的演变史:从回调到Promises再到Async/Await
我最喜欢的网站之一是BerkshireHathaway.com--它简单,有效,并且自1997年推出以来一直正常运行.更值得注意的是,在过去的20年中,这个网站很有可能从未出现过错误.为什么?因为它都 ...
- 【译】Async/Await(三)——Aysnc/Await模式
原文标题:Async/Await 原文链接:https://os.phil-opp.com/async-await/#multitasking 公众号: Rust 碎碎念 翻译 by: Praying ...
- 【译】Async/Await(四)—— Pinning
原文标题:Async/Await 原文链接:https://os.phil-opp.com/async-await/#multitasking 公众号: Rust 碎碎念 翻译 by: Praying ...
随机推荐
- 数组问题:a[i][j] 和 a[j][i] 有什么区别?
本文以一个简单的程序开头--数组赋值: int LEN = 10000; int[][] arr = new int[LEN][LEN]; for (int i = 0; i < LEN; i+ ...
- Spring中毒太深,离开Spring我居然连最基本的接口都不会写了
前言 随着 Spring 的崛起以及其功能的完善,现在可能绝大部分项目的开发都是使用 Spring(全家桶) 来进行开发,Spring也确实和其名字一样,是开发者的春天,Spring 解放了程序员的双 ...
- Python爬虫之BeautifulSoup库
1. BeautifulSoup 1.1 解析库 1)Python标准库 # 使用方法 BeautifulSoup(markup, "html.parser") # 优势 Pyth ...
- .net core WebAPI性能监控-MiniProfiler与Swagger集成
------------恢复内容开始------------ 安装Nuget Install-Package MiniProfiler.AspNetCore.Mvc Install-Package M ...
- mysql: SOURCE error 2?
mysql: SOURCE error 2? mysql -uroot -p ****** # 路径输入错误,会抛出这个异常 mysql> source /var/lib/ambari-serv ...
- vue第十五单元(熟练使用vue-router插件)
第十五单元(熟练使用vue-router插件) #课程目标 1.掌握路由嵌套 2.掌握导航守卫 #知识点 #一.路由嵌套 很多时候,我们会在一个视口中实现局部页面的切换.这时候就需要到了嵌套路由. 也 ...
- Spark的RPC
Spark 的 RPC 什么是RPC 在Spark中很多地方都涉及网络通信,比如Spark各个组件间的消息互通.用户文件与Jar包的上传.节 点间的Shuffle过程.Block数据的复制与备份等. ...
- vue实现点击样式高亮
•在data中定义即将渲染的数据,及active data() { return { active:'',//选中样式 }; }, 1 2 3 4 5 6 7 8 9 ...
- Spring中BeanFactory与FactoryBean到底有什么区别?
一.BeanFactory BeanFactory是一个接口,它是Spring中工厂的顶层规范,是SpringIoc容器的核心接口,它定义了getBean().containsBean()等管理Bea ...
- C#反编译工具ILSpy 反汇编
ILSpy反编译工具之C#反汇编 1.下载ILspy工具 https://github.com/icsharpcode/ILSpy#ilspy------- 注意: ILspy需要在电脑上安装.N ...