回顾过去,我认为可以公平地说,Go在让编程界相信并发是一种强大工具方面发挥了重要作用,特别是在多核网络世界中,它可以比pthread做得更好。如今,大多数主流语言都对并发提供了很好地支持。
另外,Go的并发版本在导致它出现的语言线中有些新颖,因为它使goroutine变得平淡无奇。没有协程,没有任务,没有线程,没有名称,只有goroutine。我们发明了“goroutine”这个词,因为没有适合的现有术语。时至今日,我仍然希望Unix的拼写命令可以学会它。
顺便说一句,因为我经常被问到,让我花一分钟时间谈谈async/await。看到async/await模型及其相关风格成为许多语言选择支持并发的方式,我有点难过,但它肯定是对pthreads的巨大改进。
与goroutine、channel和select相比,async/await对语言实现者来说更容易也更小,可以更容易地内建或后移植到现有平台中。但它将一些复杂性推回给了程序员,通常会导致Bob Nystrom所著名的“彩色函数”。
我认为Go表明了CSP这种不同但更古老的模型可以完美地嵌入到过程化语言中,没有这种复杂性。我甚至看到它几次作为库实现。但它的实现,如果做得好,需要显著的运行时复杂性,我可以理解为什么一些人更倾向于不在他们的系统中内置它。不管你提供什么并发模型,重要的是只提供一次,因为一个环境提供多个并发实现可能会很麻烦。Go当然通过把它放在语言中而不是库中解决了这个问题。
https://tonybai.com/2024/01/07/what-we-got-right-what-we-got-wrong/
https://github.com/rustaceanclub/rust-slides/tree/master/20191207-westar
20191123-sh/Tokio Futures Rust(pub).pdf
水生分享的…
https://priver.dev/blog/rust/multi-threading/
2019-coscup2019/coscup2019-our-future-in-rust.pdf
陈明煜-2023RustChinaConf.pptx
异步编程是一种编程范式,它允许程序在等待某个操作完成时继续执行其他任务。这样可以提高程序的响应速度和吞吐量。
通过 async 标记的语法块会被转换成实现了Future特征的状态机。与同步调用阻塞当前线程不同,当Future执行并遇到阻塞时,它会让出当前线程的控制权,等待其他 Future 的执行结果。
与block_on不同,.await并不会阻塞当前的线程,而是异步的等待Future A的完成,在等待的过程中,该线程还可以继续执行其它的Future B,最终实现了并发处理的效果。
await对于实现异步编程至关重要,它允许我们在同一个线程内并发的运行多个任务,而不是一个一个先后完成
https://juejin.cn/post/7214007449907740732
https://www.bilibili.com/video/BV1Ki4y1C7gj
大家好
我是杨旭
这门课程啊
给大家介绍一下rust的异步编程
这个课程配套的教程就是官方教程
我们来看一下
就是这个网址
你在网上搜一下rust async
就可以搜到这本书
这是一本在线的书
这就是官方的一个在线的教程
我们完全是按照这个上节来讲的好
今天是第一节
讲一下第一章
1. Getting started
先看一下一点一
我们为什么要使用ASYNC
也就是异步编程
为什么呢
i think编程也叫异步编程
它是一种并发的编程模型
这个并发英文单词是concurrent
它并不是并行的意思
具体来说呀
他就是允许你在少数系统线程上运行
大量的并发的任务
我们使用的语法呢就是ASYNC和await
这个语法
这个语法看起来和同步的编程是差不多的
好我们继续我们简单介绍一下其他的并发模型
我们介绍这四种
第一种就是系统线程
OS线程这种模型它无需改变编程的模型
但是线程间的同步是比较困难的
性能开销是比较大的
而使用线程池的话
确实是可以降低一些开销成本
但是仍然是难以支撑大量的NIO绑定的工作
第二个模型啊是event driven事件驱动的这个编程
这个模型是与回调函数一起用的
它有可能是比较高效的
但是它这个控制流啊是非线性的
数据流和这个错误传播是比较难以追踪的
第三个呢是cot
它类似线程也是无需改变编程的模型
他和这个AC也是比较类似的
可以支持大量的任务
但是啊它是抽象掉了底层的细节
而底层细节呢
它对系统编程以及制定运行时的实现
都是很重要的
所以说有些场景有些语言可以使用客户听
而有些语言比如说面向系统编程的语言
那么使用coo听啊
好像就不是十分合适了
最后还有一个actor模型
它就是将所有并发计划分为actor
但是这个消息通信呢是比较容易出错的
这个模型啊实现起来可以是比较高效的
但是仍然有许多实际的问题还没有解决
比如说流控制
从事逻辑等等
好
这是其他的四种并发模型
我们看一下rust里面使用的ASYNC
这个异步模型有这么几个东西
这里边有一个future
这个以后我们会讲到哈
我们先大概了解一下future啊
它是惰性的
是lazy的
只有在调用库函数的时候
他这个才能取得进展
而被丢弃的这个future就无法取得进展了
我们这个就先了解一下就行
然后这个rust里面
这个a sink是所谓的零成本的
使用ASYNC可以无需堆内存的分配
也无需这个动态调度
这对性能啊是比较好的
而且呀允许在受限环境中使用这个ASYNC
再有啊就是rust它不提供内置的运行时
运行时啊都是由社区提供的
比如说TOKIO和这个ASSISTD等等吧
然后这个单线程多线程它都是支持的
但优缺点呢是不一样的
我们继续看看rust中的这个ASYNC和线程
线程啊
就是系统线程线程适用于少量的任务
它有内存和CPU的开销
而且线程的生成啊
和这个线程之间的切换呢是比较昂贵的
线程池确实可以降低一些开销成本
系统线程啊
它允许从用同步的代码
也就是代码无需大改
无需特定的编程模型
而且有些系统啊支持修改线程的优先级
这是系统线程的特点
我们看一下rust async
它可以显著降低内存和CPU的开销
而且在同等条件下
它支持比系统线程多几个数量级的任务
具体呢就是它使用少量的线程
来支持大量的并发任务
但是它的可执行文件是比较大的
因为其实呀它是需要生成状态机的
而且每个可执行文件都需要捆绑一个异步
运行时
注意哈
这个i think异步编程并不是比线程好
只是它们不同而已
针对不同的场景
有可能我选择线程
也有可能选择是AC好继续看这个例子
这个例子啊
我们就是想从两个网站下载网页
我们想并发的下载这两个网页
第一种方式呢在这的是使用的系统线程
Threspan
这里创建了两个线程
但是下载网页这任务实在是太小了
创建线程来做这个工作啊
实际上就是一种巨大的浪费
而如果我们需要下载成千上万个网页的话
在使用这种方式的话
那么这块就会成为一个瓶颈
这就是使用系统线程的方式
而下面这种方式呢
就相当于使用rust async的写法
看看这个FN前面有个a sic关键字
然后里边这个写法啊
跟同步的代码可能是有点不同
可能得改一下
但是呢这里边函数的调用都是静态分配的
而且也没有堆内存的分配
最重要的是它没有创建额外的线程
那具体这个怎么回事啊
啊我们以后再讲
就把我刚才说的那几点了解一下就行好
继续再讲一下自定义并发模型
除了线程和ACC以外
你还可以使用其他的并发模型
比如说之前介绍的event driven等等
这都是可以的
好我们看一下1.2
现在这个rust AC目前的状态
他目前状态是这样的哈
啊一部分可能是一大部分是比较稳定的
但是还有一部分它仍然在变化
rust async的特点
哪有这么几个针对典型的并发任务的时候啊
它的性能是非常出色
与高级语言特性频繁交互
比如说生命周期和这个ping ping就叫pin吧
同步和异步代码间
以及这个不同运行时的异步代码间
存在兼容性的约束
刚才说了
这个运行时不是官方提供的
是由社区提供的
有不同的异步运行时
他们这个之间的异步代码
可能存在一些兼容性的约束
而且由于这个不断进化
未来这个维护这个异步代码啊
可能会这个负担会更重一些
好我们继续
我们再看一下语言和库的知识
虽然rust语言本身就支持这个ASYNC编程
但是很多应用啊依赖于社区的库标准库啊
就提供了最基本的特性类型和功能
比如说这个future treat
而这个async await这个语法呀
它是直接被rust编译器所支持的
Future create
这是一个单独的create
它提供了许多的实用类型
宏和函数
它们可以用于任何异步应用程序
异步代码IO和任务生成的执行
由这个a theme times提供
也就是所谓的异步运行时
比如说TOKIO
比如说这个async std
大多数这个ASYNC和应用程序和一些async create
都依赖于特定的运行时
TOKIO和这个AC s t d可能是目前比较流行的
有一点注意哈
rust不允许你在chat里边声明AC函数
看一下编译和调试编译错误
由于这个async rust
通常依赖于更复杂的语言功能
例如生命周期和这个ping这个拼音以后
我们会讲
因此呢可能会更频繁的遇到这些类型的错误
第二个就是运行时错误
每当运行时遇到异步函数
编译器呢会在后台生成一个状态机
然后这个stack trace里边就有他的明细
以及这个运行时调用的函数
因此呢解释起来会更复杂一些
还有一个特点呢
就是新的失效模式可能会出现一些新的故障
而且这些故障啊他们有时候是可以通过编译的
甚至是可以通过单元测试
有时候是难以发现的
但是一运行就可能出现错误
兼容性考虑ASYNC就是异步的
它和同步的代码不能总是自由组合的
举个典型的例子
就是我们不能直接在同步函数里面来调用
异步函数
再就是异步代码间也不总是能够自由组合的
有一些create呀
它依赖于特定的AC的运行时
它就可能存在一些兼容性约束的问题
因此你在做项目的时候
应该尽早研究到底是使用哪个AC运行时
尽早定下来
然后我们看一下性能的特征
ASYNC的性能啊
依赖于运行时的实现
反正你记住通常这个性能是比较出色的
好看一下1.3
ASYNC和await的入门
就简单讲一下
这一节呢不要求完全的理解
有个印象就行
先看一下ASYNC
ASYNC它就是异步的意思
它会把一段代码转化为一个
实现了future treat的状态机
虽然在同步方法中调用阻塞函数
会阻塞整个线程
但是阻塞的future啊将放弃对线程的控制
从而允许其他future来运行好
我们可以先看一下这个异步函数是什么样的
我们创建一个项目
这个项目已经创建好了
这我就不讲了
有一个依赖依赖于future
这create它的版本目前是0.3好
然后我们看一下面点rs在面函数里边啊
我们声明了一个异步的函数
它语法就是这样的
在这个FN前面加上这个ASYNC这个单词
这个关键字这个异步函数目前里面什么都没干
但它的语法就是这样的好我们回来我们继续
异步函数的语法就是这样的
FN前面加上AC关键字
而a think i fn也是异步函数
它的返回类型是一个future
这个future啊
它是未来的意思
future啊
它需要由一个执行者来运行
什么执行者呢
我们来举个例子
执行者的英文呢就是executor
有这么一个执行者叫block on
block on啊
它会阻塞当前的线程
直到提供了这个future运行完成为止
我们可以先看一个例子
这个例子这是一个异步函数对吧
然后里边呢就是打印了一个hello world
然后我们把这个block这个执行者
这个executor给引入进来
future executter block它是一个函数
然后在main函数里边呢
首先调用这个异步函数
它返回类型是一个future
这就是一个future
看它的类型就是一个future对吧
这行走完了
什么都没有打印出来
这个hello word并没有打印出来
怎么才能打印的
就是这个返回的future
他得在一个executor
或者叫执行者上边才能运行
所以说呢把它传给这个block这个执行者
这个时候QQ才运行才能取得进展对吧
所以这个时候他就打印出来了
Hello world
我们再回来再说一遍
block会阻塞当前的线程
直到给他提供的这个QQ运行完成为止
当然我们还有其他的执行者
他们呢能够提供更为复杂的行为
比如说将多个future安排到同一个线程上运行好
我们继续看一下await
在这个异步函数中
可以使用点await
来等待另一个实现了future treat的类型的完成
这个await呀
它和block on是不同的
点await并不会阻塞当前的线程
而是异步的等待这个future的完成
也就是说如果这个future目前无法取得进展
那么就允许其他任务运行
我们看这样一个例子
主要看这这有三个异步函数
实际上就是要做三件事
一个是学习唱歌
一个是唱歌对吧
然后一个是跳舞
然后在main函数里边
我们首先使用block on这种方式来执行这三件事
这就是三个block
但是很显然这种做法没有提供最佳的性能实现
因为我们是blog是主色的
所以说每次只能干一件事儿
我们只能先学完唱歌
然后再唱歌
然后唱完歌呢再跳舞
正常情况下我们可以一边学习和唱歌
然后与此同时我们还进行跳舞
所以可以并发的执行
这样的话性能会更好一些好
那我们就换一种实现
我暂停一下
我们来看一下前面这三个函数没变
然后我首先呢加了这么一个函数
Learn and c
首先在这里调用了这个学习唱歌这个异步函数
然后使用点await
点await之后
相当于这个学习的过程就完事了
然后我们就可以唱歌了
然后就是唱歌点await
而使用点await来代替这个block on啊
就不会导致这个线程的阻塞
从而就有可能让我们在唱歌的同时
可以来进行跳舞
然后下边儿啊我们又创建了一个异步的幂函数
这里边首先调用这个learn and sing
这个函数是异步的
返回的是一个future叫F1
然后dance也是返回一个future叫F2
然后我们使用这个future转这个红
Join
这个红啊和这个await有点像
但是他可以等待多个future
现在如果我们阻塞在这个learn and sing
也就是F1这个future这块
那么这个F2future就会接管整个线程
反过来也一样
如果我们阻塞在F2
那么F1这个future也会接管整个线程
而如果这两个future都阻塞的话
我们就说这个a theme这个函数它就是阻塞了
然后接下来的事就交给他的执行者
他执行者在这儿了
就交给他了
好这个例子就到这儿
第一节也就到这了
谢谢大家
2. future trait
好我们开始第二章
第二章呢讲的是一个背后的原理
就是执行future和任务
这一节第二节一节
我们首先讲一下future这个trade future trait啊
它是rust a c编程的核心
这个官网上说呀
future它是一种异步的计算
它可以产生一个值
而具体来说呢实现了future这个twitter的类型啊
它表示的是一种目前可能还不可用的值
未来可能可用
就是这么个意思好
下面我们看一下这个简化版的future treat
它叫simple future
简化版的吗
它有一个这么站位的一个类型
output
因为我们刚刚得到future的时候啊
它里边要返回的这个时通常是暂时是不可用的
但是未来它会返回一个值
这个值是什么类型呢
就是这个output类型
然后呢他还有个方法叫pull
pull这个单词是什么意思呢
还有调查的意思
puling加上这个ng就是轮询的意思
他也有点这个意思
调用这个铺方法呀
就会驱动这个future尽可能的向着完成来前进
我们看到它的参数啊
有一个wake
它是一个函数指针
它呢有点像是一个回调函数
然后这个铺方法的返回结果呢就是一个铺
枚举这个玩具有两个变体
一个是ready
如果返回的是ready
就表示这个future啊它已经结束了
q q结束之后还得返回一个值
这个值是什么类型
就是传进去的output
这个类型你想让它是什么类型
就是什么类型
另外一种情况呢就是调用这个put方法
它会返回这个判定
就表示这个future啊还没有结束
未来至少还得再铺一次
看看到时候的进展好
我们来举个例子哈
这个future啊到底可以表示什么呢
我举了三个例子
例如它可以表示下一次网络数据包的到来
但目前还没有到来
但是下一次总会有这个数据包到来
他呢还可以表示下一次鼠标的移动
现在这个鼠标可能没动
但过一会儿他可能就动了
这就是下一次的移动
或者呢仅仅表示经过一段时间的那个时间点
比如说过了五秒
五秒之后
这个时间点这个future就可以表示这个好
我们继续从另外一个角度
我们可以这样来解释这个future future
它代表着一种你可以检验其是否完成的这么一个操作
羞耻就是用来检验的
你通过什么来检验呢
通过铺函数来检验
铺函数返回那枚举的ready就表示它完成了
如果返回的是判定这个辩题
就表明这个future还没有完成
所以说put函数是用来检验这个future是否完成的
也可以这么说
future可以通过调用这个put方法或者叫破函数来取得进展
铺函数啊
会驱动这个future尽可能的接近完成
而调用库函数返回的结果啊
就是那个枚举
如果返回的是ready这个变体
就表示这个future完成了
那么future完成之后返回的数据是什么呢
就是这个ready里面的result
这个result就是最终的结果
这时候呢我们就说这个future它就解析成了这个result
而如果这个副函数返回的是pending这个辩题
我们就说这个future啊暂时还没法完成
而未来某一时刻这个future啊
他就会准备好取得更多的进展
而qq准备好取得更多进展呢
这就相当于是一个事件
而当一个事件发生的时候啊
我们通常是使用一个回调函数对吧
而这future这里就有一个这么个东西啊
就是一个waker
一个唤醒者
这个唤醒者这个waker上面有一个wake函数
这个呢就相当于是这个事件的一个回调函数
也就是说当future准备好取得更多的进展的时候
你就调用这个v k函数好
接下来我们用一句话来描述一下这个future
针对future你唯一能做的就是使用p这个方法来敲它
来戳它
直到从他那里戳出来一个值为止
或者是错出来一个错误
好继续
我们再来看一下这个v函数
就是当这个future准备好取得更多进展的时候
我们就调用这个函数
而当这个函数调用的时候
执行器执行者executor就会驱动这个future再次调用这个库函数
你把我唤醒了
把我wake了
那我就再次检查一下
你通过这个库函数来检查
从而呢让这个future能够取得更多的进展
最大的进展呢就是完成把最终的那个值取出来对吧
如果没有这个wake函数
那么执行器就不知道这个特定的future何时能取得进展
这样的话
我们就只能通过不断的调用库函数来检查这个future到底取没取得进展
这肯定是低效的
而通过wake函数执行器啊
就会确切的知道哪些future已经准备好进行铺的调用了
好我们继续我们看一个例子啊
这一个socket socket实现了这个future它未来产生结果的类型就是vector u8
然后看一下它的put函数或者叫复方法
这里有个wake函数返回的是p这个枚举好
每次调用库函数的时候
就说明这个future这个socket ra他已经准备好取得更多的进展了
然后我们检查一下
看一下这个socket是否有数据可以读
而如果有数据这块就是醋
然后就找到这一块了
那么就把这个数据读出来并返回
所以说返回的就是puri
我已经准备好取得更多的进展了
更多的进展是什么呢
我就把这个数据读出来了对吧
否则的话目前这个socket还没有数据
没有数据怎么办呢
就是当未来有数据的时候
或者说我这个future准备好取得更多进展的时候啊
你得告诉我怎么告诉呢
就是使用wake这个函数就相当于是个回调函数
我们就把这个wake传到这里边了
这块我们不知道具体是什么
但是我们知道当有数据的时候
这个wake就会被调用
而这时候呢返回的就是pending这个辩题
而如果未来有数据了
那么这个wake就会被调用
调用之后呢
他就会再次调用这个future的p这个方法来检查他到底准备准备好好
我们看下一个例子
这个例子呢就是组合了多个异步操作
而且无需中间分配
可以通过无分配的状态机来实现多个future同时运行或者是串联运行
这个呢就简单看一下吧
有这么一个struck叫join
它里边有两个future
而他的目的呢就是运行两个future来并发的
让他们完成其中啊
这两个future有可能是交错的
而如果某个future已经完成的话
那么相应的字段比如说a就会设置成
当这样的话
在它完成之后
我们就无法对它进行再次破了对吧
因为它变成n好
然后我们为这个转这个struct来实现这个simple future
他最后啊实际上没有什么返回值
然后看一下它的这个铺函数铺方法
先看一下a这个时段对吧
如果a有值
那我们看看这个a的状态
它是不是ready
如果是的话
就把里边的future取出来
然后给它设置成了使用的是take这个方法
然后使用同样的逻辑来看一下这个b最后如果a是nb也是
那就说明这两个future啊都已经完成了
都已经准备好了
所以说呢这球就返回的是普ready
否则的话返回的就是popending pending
就表示至少还有一个future没有完成工作好
这是这个例子
然后我们看下一个例子
就是多个连续的q q啊
可以一个接一个的运行
我们来看一下
有这么一个start叫暗镇future
首先有第一个字段first啊
它是一个option类型的future对吧
future a
然后第二个字段呢是future b
然后为这个struct实现simple future
看一下它的put方法
如果他第一个字段有值对吧有值
然后就调用它的put方法
如果返回的是ready
那么就表示第一个完成了
这时候就把第一个呀给它设置成闹
然后呢我们再执行第二个字段这个future
否则的话第一个没完成
我们就返回pup
定要等着第一个完成之后才能做第二个
这就是这么个例子好最后呢我们看一下真正的这个future treat treat
future type output
这块是一样的
fn p这块也是一样的
然后self的self不一样的
刚才我们simple future self的类型是muse
而这个类型呢是ping mute self
这个pin mute self我们以后会讲
简单来说呢就是它允许我们创建不可移动的qq
而不可移动的对象呢可以在他们的字段间存储指针这些文管
而如果需要启用ac await这个语法的话
这个pin呢它就是必须的
这是一个变化
第二个变化呢是第二个参数
原来是fn这个函数传递的是一个函数对吧
这个时候呢变成了一个mute context
在我们simple future里啊
我们是通过调用函数指针这个fn来告诉future的执行器
相关的future应该被曝了
就是一个make函数
而由于fn呢它只是一个函数指针
它不能存储这个数据
哪个数据啊
就是关于哪个pure调用了wink的数据
就是不能存储一些上下文数据
而这个context类型啊
他就提供了访问waker类型的值的方式
这些词啊可以被用来wake up
也是唤醒特定的任务
这以后我们会讲我们举个例子
就是在实际项目中
例如这个web server
它有可能有上千个不同的连接
而他们的wake up呢
他们的换血应该是单独管理的啊
最后这一块呢就简单了解一下
好这局就到这儿
谢谢大家
原文链接: https://dashen.tech/2023/12/11/Rust-Async-异步编程-详细版/
版权声明: 转载请注明出处.