原文标题: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. git clone 速度太慢解决方法

    本来想下载一个翻墙软件,实在是忍受不了每秒十几K的龟速,查阅各种资料,终于找到了失传已久的秘籍 先附图,实测有效,这速度简直要上天了啊啊啊啊啊(只支持HTTPS方式,SSH无效) 方案:使用githu ...

  2. STL——容器(Set & multiset)的默认构造 & 带参构造 & 对象的拷贝构造与赋值

    1. 默认构造 set<int> setInt;              //一个存放int的set容器. set<float> setFloat;          //一 ...

  3. 【Django】django.core.exceptions.ImproperlyConfigured: mysqlclient 1.4.0 or newer is required;

    报错信息 django.core.exceptions.ImproperlyConfigured: mysqlclient 1.4.0 or newer is required; you have 0 ...

  4. 淘宝|蚂蚁|菜鸟|盒马|嘀嘀|饿了么面经(已拿多个offer)

    上一篇的同学拿到了bigo和腾讯的offer,这一次的分享来自两位同学的面试综合,他们分别拿到了菜鸟.嘀嘀.盒马的多个offer,由于面试的时间跨度时间太长,且面试的部门太多,只能回忆到具体的面试题, ...

  5. CAP、BASE、ACID

    CAP定理 定义 CAP定理(CAP theorem),它指出对于一个分布式计算系统来说,不可能同时满足以下三点: 一致性(Consistency) (等同于所有节点访问同一份最新的数据副本:写操作之 ...

  6. Flink开发中的问题

    1. 流与批处理的区别 流处理系统 流处理系统,其节点间数据传输的标准模型是:当一条数据被处理完成后,序列化到缓存中,然后立刻通过网络传输到下一个节点,由下一个节点继续处理. 批处理系统 批处理系统, ...

  7. 2020软件测试工程师面试题汇总(内含答案)-看完BATJ面试官对你竖起大拇指!

    测试技术面试题 1.什么是兼容性测试?兼容性测试侧重哪些方面? 参考答案: 兼容测试主要是检查软件在不同的硬件平台.软件平台上是否可以正常的运行,即是通常说的软件的可移植性. 兼容的类型,如果细分的话 ...

  8. 从零到一快速搭建个人博客网站(域名自动跳转www,二级域名使用)(二)

    前言 本篇文章是对上篇文章从零到一快速搭建个人博客网站(域名备案 + https免费证书)(一)的完善,比如域名自动跳转www.二级域名使用等. 域名自动跳转www 这里对上篇域名访问进行优化,首先支 ...

  9. JDK8新特性详解(一)

    虽然JDK8已经出来了N久,其新特性也在日益改变着我们的编码习惯和风格.虽然有些新特性用起来很顺手,但是总是傻傻分不清到底是哪个版本的.趁今天有时间,我们就来总结一下,JDK8有哪些能提升我们开发效率 ...

  10. jquery ajax return不起作用

    jquery的ajax方法:在success中使用return:来结束程序的时候,结束的只是success这个方法,也就是说success中的return的作用范围只是success: 如果要想在su ...