零成本异步 I/O (上)

2024-06-23 00:48
文章标签 异步 成本

本文主要是介绍零成本异步 I/O (上),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

这是 Withoutboats 在 2019 年 4 月的 Rust Latam 上所做报告的一个整理。这个报告主要介绍他参与开发了一年半的语言特性,包括 Rust 异步 I/O 的发展历程,以及目前已经稳定的零成本抽象的async/await 语法的关键实现原理。

Withoutboats 是就职于 Mozilla 的一名研究员,主要从事 Rust 语言开发。他开发的这个语言特性叫做 async/await,这可能是本年度我们在 Rust 语言上做的最重要的事。这解决了困扰我们很久的问题,即我们如何能在 Rust 中拥有零成本抽象的异步IO。

注:因讲稿篇幅较长,所以分成上下两部分;上主要介绍 Rust 异步 I/O 的发展历程,下主要介绍目前的零成本抽象的实现原理;因个人水平有限,翻译和整理难免有错误或疏漏之处,欢迎读者批评指正。

async/await

首先,介绍一下 async/await。

async 是一个修饰符,它可以应用在函数上,这种函数不会在调用时一句句运行完成,而是立即返回一个 Future 对象,这个 Future 对象最终将给出这个函数的实际返回结果。而在一个这样的 async 函数中,我们可以使用await运算符,将它用在其它会返回 Future 的函数上,直到那些 Future 返回实际结果。通过这种方法,异步并发开发更加方便了。

let user = await db.get_user("withoutboats");
impl Database {async fn get_user(&mut self, user: &str) -> User {let sql = format!("select FROM users WHERE username = {}", user);let db_response = await self.query(&sql); User::from(db_response) }}

这是一段简短的代码样例,我们具体解释一下 Future 。这段代码基本上做的就是一种类似于 ORM 框架所作的事。你有一个叫 get_user 的函数,它接受一个字符串类型的用户名参数,并通过在数据库中查找对应用户的记录来返回一个User对象。它使用的是异步 I/O ,这意味着它得是一个异步函数,而不是普通函数,因此当你调用它时,你可以异步等待(await)它;然后我们看一下函数的实现,首先是用用户名参数拼接出要执行的 SQL 语句,然后是查询数据库,这就是我们实际执行 I/O 的地方,所以这个查询(query)返回的是 Future ,因为它使用的是异步 I/O 。所以在查询数据库时,你只需要使用异步等待(await)来等待响应,在获得响应后就可以从中解析出用户。这个函数看起来像个玩具,但我想强调的是,它与使用阻塞式 I/O 的唯一区别就是这些注解(指async/await)了,你只需将函数标记为异步(async),并在调用它们时加上 await 就行了,开发的心智负担很小,以至于你会忘了自己是在写异步 I/O 而不是阻塞 I/O 。而 Rust 的这种实现让我尤其感到兴奋的是,它的 async/await 和 Future 都是零成本抽象的。

零成本抽象

零成本抽象是 Rust 比较独特的一项准则,这是使 Rust 与其他许多语言相区别的原因之一。在添加新功能时,我们非常关心这些新功能是不是零成本的。不过这并不是我们想出来的点子,它在 C++ 中也很重要,所以我认为最好的解释是 Bjarne Stroustrup 的这句话:

零成本抽象意味着你不使用的东西,你不用为它付出任何代价,进一步讲,你使用的东西,你无法写出比这更好的代码。

Zero Cost Abstractions: What you don’t use, you don’t pay for. And further: What you do use, you couldn’t hand code any better.

也就是说零成本抽象有两个方面:

  1. 该功能不会给不使用该功能的用户增加成本,因此我们不能为了增加新的特性而增加那些会减慢所有程序运行的全局性开销。

  2. 当你确实要使用该功能时,它的速度不会比不使用它的速度慢。如果你觉得,我想使用这个非常好用的功能把开发工作变得轻松,但是它会使我的程序变慢,所以我打算自己造一个,那么这实际上是带来了更大的痛苦。

所以,我将回顾一下我们如何尝试解决异步 I/O 和 Rust 的问题,以及在我们实现这一目标的过程中,某些未能通过这两项零成本测试的特性。

绿色线程的尝试

我们要解决的问题是 异步 I/O 。通常 I/O 处于阻塞状态,因此当你使用 I/O 时,它会阻塞线程,中止你的程序,然后必须通过操作系统重新调度。阻塞式 I/O 的问题是当你尝试通过同一程序提供大量连接时,它无法真正实现扩展。因此对于真正的大规模网络服务,你需要某种形式的非阻塞的或者说异步的 I/O 尤其是 Rust 是针对具有这些真正高性能要求而设计的语言,它是一种系统编程语言,面向那些真正在乎计算资源的人。要在网络的世界中真正取得成功,我们就需要某种解决方案来解决这个异步 I/O 问题。

但是 异步 I/O 的最大问题是它的工作方式 :在你调用 I/O 时,系统调用会立即返回,然后你可以继续进行其他工作,但你的程序需要决定如何回到调用该异步 I/O 暂停的那个任务线上,这就使得在编码上,异步 I/O 的代码要比阻塞 I/O 的代码复杂得多。所以,很多,尤其是以可扩展的网络服务这类特性为目标的语言,一直在试图解决这个问题。比如,让它不再是最终用户需要解决的问题,而是编程语言的一部分或者某个库的一部分等等。

Rust 最初使用的第一个解决方案是 绿色线程,它已经在许多语言中获得成功。绿色线程基本上就像阻塞式 I/O 一样,使用的时候就像是普通的线程,它们会在执行 I/O 时阻塞,一切看起来就跟你在使用操作系统的原生方式一样。但是,它们被设计为语言运行时的一部分,来对那些需要同时运行成千上万甚至数百万个绿色线程的网络服务用例进行优化。一个使用该模型的典型的成功案例就是 Go 语言,它的绿色线程被称为 goroutine。对于 Go 程序来说,同时运行成千上万个 goroutine 是很正常的,因为与操作系统线程不同,创建它们的成本很低。


操作系统线程绿色线程
内存开销较大的堆栈,增加大量内存占用初始堆栈非常小
CPU开销上下文切换至操作系统的调度器,成本很高由程序本身的运行时调度

即 绿色线程的优点 在于,产生操作系统线程时的内存开销要高得多,因为每个操作系统线程会创建一个很大的堆栈,而绿色线程通常的工作方式是,你将产生一个以很小的堆栈,它只会随着时间的推移而增长,而产生一堆不使用大量内存的新线程并不便宜;并且使用类似操作系统原语的问题还在于你依赖于操作系统调度,这意味着你必须从程序的内存空间切换到内核空间,如果成千上万的线程都在快速切换,上下文切换就会增加很多开销。而将调度保持在同一程序中,你将避免使用这些上下文,进而减少开销。所以我相信绿色线程是一个非常好的模型,适用于许多语言,包括 Go 和 Java。
 
在很长一段时间内, Rust 都有绿色线程,但是在 1.0 版本之前删掉了。我们删掉它是因为它不是零成本抽象的,准确的说就是我在第一个问题中谈到的,它给那些不需要它的人增加了成本。比如你只想编写一个不是网络服务的屏幕打印的 Rust 程序,你必须引入负责调度所有绿色线程的语言运行时。这种方法,尤其是对于试图把 Rust 集成到一个大的 C 应用程序中的人来说,就成为一个问题。很多 Rust 的采用者拥有一些大型C程序,他们想开始使用 Rust 并将 Rust 集成到他们的程序中,只是一小段 Rust 代码。问题是,如果你必须设置运行时才能调用 Rust ,那么这一小部分的 Rust 程序的成本就太高了。因此从 1.0 开始,我们就从语言中删除了绿色线程,并删除了语言的运行时。现在我们都知道它的运行时与 C 基本上相同,这就使得在 Rust 和 C 之间调用非常容易,而且成本很低,这是使 Rust 真正成功的关键因素之一。删除了绿色线程,我们还是需要某种异步 I/O 解决方案;但是我们意识到 这应该是一个基于库的解决方案,我们需要为异步 I/O 提供良好的抽象,它不是语言的一部分,也不是每个程序附带的运行时的一部分,只是可选的并按需使用的库。

Future 的解决方案

最成功的库解决方案是一个叫做 Future 的概念,在 JavaScript 中也叫做 Promise。Future 表示一个尚未得出的值,你可以在它被解决(resolved)以得出那个值之前对它进行各种操作。在许多语言中,对 Future 所做的工作并不多,这种实现支持很多特性比如组合器(Combinator),尤其是能让我们在此基础上实现更符合人体工程学的 async/await 语法。

Future 可以表示各种各样的东西,尤其适用于表示异步 I/O :当你发起一次网络请求时,你将立即获得一个 Future 对象,而一旦网络请求完成,它将返回任何响应可能包含的值;你也可以表示诸如“超时”之类的东西,“超时”其实就是一个在过了特定时间后被解决的 Future ;甚至不属于 I/O 的工作或者需要放到某个线程池中运行的CPU密集型的工作,也可以通过一个 Future 来表示,这个 Future 将会在线程池完成工作后被解决。

trait Future {    type Output;    fn schedule<F>(self, callback: F)        where F: FnOnce(Self::Output);}

Future 存在的问题 是它在大多数语言中的表示方式是这种基于回调的方法,使用这种方式时,你可以指定在 Future 被解决之后运行什么回调函数。也就是说, Future 负责弄清楚什么时候被解决,无论你的回调是什么,它都会运行;而所有的不便也都建立在此模型上,它非常难用,因为已经有很多开发者进行了大量的尝试,发现他们不得不写很多分配性的代码以及使用动态派发;实际上,你尝试调度的每个回调都必须获得自己独立的存储空间,例如 crate 对象、堆内存分配,这些分配以及动态派发无处不在。这种方法没有满足零成本抽象的第二个原则,如果你要使用它,它将比你自己写要慢很多,那你为什么还要用它。

本文来自耿腾兄的投稿,感谢辛苦的付出。

原文地址:https://blog.gengteng.online/2019/12/02/zero-cost-async-io-1/?from=singlemessage&isappinstalled=0

这篇关于零成本异步 I/O (上)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1085866

相关文章

人工和AI大语言模型成本对比 ai语音模型

这里既有AI,又有生活大道理,无数渺小的思考填满了一生。 上一专题搭建了一套GMM-HMM系统,来识别连续0123456789的英文语音。 但若不是仅针对数字,而是所有普通词汇,可能达到十几万个词,解码过程将非常复杂,识别结果组合太多,识别结果不会理想。因此只有声学模型是完全不够的,需要引入语言模型来约束识别结果。让“今天天气很好”的概率高于“今天天汽很好”的概率,得到声学模型概率高,又符合表达

Rust:Future、async 异步代码机制示例与分析

0. 异步、并发、并行、进程、协程概念梳理 Rust 的异步机制不是多线程或多进程,而是基于协程(或称为轻量级线程、微线程)的模型,这些协程可以在单个线程内并发执行。这种模型允许在单个线程中通过非阻塞的方式处理多个任务,从而实现高效的并发。 关于“并发”和“并行”的区别,这是两个经常被提及但含义不同的概念: 并发(Concurrency):指的是同时处理多个任务的能力,这些任务可能在同一时

玩转Web之easyui(二)-----easy ui 异步加载生成树节点(Tree),点击树生成tab(选项卡)

关于easy ui 异步加载生成树及点击树生成选项卡,这里直接给出代码,重点部分代码中均有注释 前台: $('#tree').tree({ url: '../servlet/School_Tree?id=-1', //向后台传送id,获取根节点lines:true,onBeforeExpand:function(node,param){ $('#tree').tree('options'

【Python】 异步编程

【Python】 异步编程 1. nest_asyncio基础定义2. nest_asyncio 举例实现基本用法 1. nest_asyncio基础定义 nest_asyncio.apply() 是 Python 编程中与异步编程相关的一个调用,它用于解决某些特定环境下的异步编程问题。下面是对这个调用的详细解释: nest_asyncio 模块:这是一个第三方库,它提供

AJAX:如何编写一个关于AJAX的Hello World?(ajax发送异步请求(四步操作))

用到的一个Servlet类: package cn.edu.web.servlet;import java.io.IOException;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;impor

iOS Runloop面试题(什么是异步绘制?)

什么是异步绘制? 异步绘制,就是可以在子线程把需要绘制的图形,提前在子线程处理好。将准备好的图像数据直接返给主线程使用,这样可以降低主线程的压力。 异步绘制的过程 要通过系统的 [view.delegate displayLayer:] 这个入口来实现异步绘制。 代理负责生成对应的 Bitmap设置该 Bitmap 为 layer.contents 属性的值。

协程: Flow 异步流 /

以异步方式返回多个返回值的方案: 在 Kotlin 协程 Coroutine 中 , 使用 suspend 挂起函数 以异步的方式 返回单个返回值肯定可以实现 , 如果要 以异步的方式 返回多个元素的返回值 , 可以使用如下方案 : 集合序列Suspend 挂起函数Flow 异步流 同步调用返回多个值的弊端   鸣谢: mAndroid面试题之Kotlin异步流、冷流Flow

极客新闻——13、美团到餐研发团队资源成本优化实践

本文笔记全部来自《极客新闻》——新鲜的技术资讯、权威的趋势剖析、别样的技术洞察 工程师主要面对的是技术挑战,更关注技术层面的目标。研发团队的管理者则会把实现项目成果和业务需求作为核心目标。在实际项目中,研发团队所需资源(比如物理机器、内存、硬盘、网络带宽等)的成本,很容易被忽略,或者在很晚才考虑。 最近,美团技术团队分享了美团到餐研发团队的资源成本优化实践。主要包括以下5点: 1、确定方

VUE\JS处理在循环中异步和同步执行的问题

业务场景: 1、有一个组别集合,每一个小组别对象里面有一个数据集合,需要循环去校验每个不同组里的数据(不同组合因为一些特殊属性不能合并到一个组里),全都符合就通过验证,去处理后续业务。 2、现在,在校验规则方法里对一个集合里的每一条数据进行强校验和弱校验,弱校验需要在提升框放入确定的操作按钮,允许通过,再循环到下一条数据的验证。 代码分析 1、涉及到组别集合的循环、组别内部数据的循环,循环套

【Rust日报】 2019-05-29:异步await语法最终确定

orkhon: 机器学习框架和运行时 #Python #ml Orkhon是用于机器学习的Rust框架,用于运行/使用用Python编写的推理/预测代码,冻结模型和处理未知(unseen)数据。 orkhon 「异步系列文章」Part 2: Async/Await语法之外的挑战 : 取消(Cancellation) #async #await 在这篇文章里,作者讨论了如果在应用中取消正