rust中运行 async 函数必须在后面加上.await,但是根据语义,代码f().await 表示要在f()完成之后才能继续运行后面的代码,那这样rust的异步不就完全变成同步了吗? async/await的好处在哪呢? 有点晕了
我设想的是,比如js
async_fn1();
async_fn2();
console.log("hello")
js的代码不会等async_fn1,async_fn2执行完再打印"hello"
但是rust代码如果像上面这样写,async_fn1和async_fn2都不会执行,他们实际返回的是一个Future,要执行就 要加上.await,但是加上.await之后,一定是async_fn1先执行,async_fn2再执行,最后打印"hello",不就又相当于变同步了吗?
想问下哪里出问题了
1
共 15 条评论, 1 页
评论区
写评论这样就是你要的同时执行的效果了,看起来和多线程一样,但是底层调度原理不一样,多线程在线程切换时,开销比较高,异步的切换开销要小很多
--
👇
ZZG: 谢谢大家,好像有点理解了,试了下join! 确实在单线程模式下也不会出现一个Future必须等另一个Future结束的情况。
现在问题是不知道如何像join!一样在单线程里同时(concurrently)让多个Future开始执行,比如join(sleep(1), sleep(2)).await 实际总共只会睡2秒而不是1秒
看了下源码也还看不懂,后面可能要再看下
可以将rust每个async函数理解成一个运行块,.await的时候,runtime可以调度执行其它运行块,这里例子中async_fn1以及async_fn2都在同一个更大的运行块中,如果async_fn1以及async_fn2后面加上了.await则代表这两个子运行块需要按照顺序执行,这样的好处就是用同步的思想写代码,代码自然而然就是异步运行的了。
我是那么理解的,即使在单线程异步执行两个任务也不会阻塞,因为运行时会在某个任务sleep时利用空闲时间执行另一个任务。如果你想得到3秒也是可能的使用std::thread::sleep,它会阻塞当前线程,而tokio::time::sleep由于是在异步运行时中等待,所以运行时会切换到其他任务,以下是GPT的解释:
单线程中两个异步任务不会互相阻塞的主要原因是基于异步编程模型的协作式调度和非阻塞I/O操作。在这种模型中,任务执行时会遇到等待点(通常是使用
.await
标记的点),在这些点上,任务会主动让出执行权,允许运行时调度其他准备就绪的任务执行。这种机制的核心要素可以总结如下:协作式调度:异步任务通过在等待点使用
.await
关键字来显式让出控制权。这种调度方式要求任务在需要等待某个操作完成时主动挂起,从而让出CPU给其他任务。非阻塞I/O操作:在异步编程中,I/O操作(如网络请求或文件读写)是非阻塞的。这意味着当一个任务发起I/O操作并等待结果时,它会被挂起,而运行时会继续执行其他任务,直到I/O操作完成并通过事件循环通知相关的任务。
事件循环:运行时内部维护一个事件循环,负责监听和响应外部事件(如I/O完成、定时器触发等)。当事件发生时,事件循环会唤醒等待该事件的任务,使其得以继续执行。在一个任务被挂起等待事件期间,事件循环可以转而执行其他任务。
通过上述机制,即使在单线程环境下,运行时也能有效地管理多个异步任务,确保它们互不阻塞,提高了单线程的工作效率。这种模型特别适合于I/O密集型任务,能够大幅提升应用程序处理并发任务的能力,而不需要增加额外的线程,从而节约资源并简化程序的并发模型。
--
👇
ZZG: 笔误,"而不是3秒"
--
👇
ZZG: 谢谢大家,好像有点理解了,试了下join! 确实在单线程模式下也不会出现一个Future必须等另一个Future结束的情况。
现在问题是不知道如何像join!一样在单线程里同时(concurrently)让多个Future开始执行,比如join(sleep(1), sleep(2)).await 实际总共只会睡2秒而不是1秒
看了下源码也还看不懂,后面可能要再看下
笔误,"而不是3秒"
--
👇
ZZG: 谢谢大家,好像有点理解了,试了下join! 确实在单线程模式下也不会出现一个Future必须等另一个Future结束的情况。
现在问题是不知道如何像join!一样在单线程里同时(concurrently)让多个Future开始执行,比如join(sleep(1), sleep(2)).await 实际总共只会睡2秒而不是1秒
看了下源码也还看不懂,后面可能要再看下
多协程并发的时候,await会切换协程运行。
比如并发很多协程发送网络请求,某个协程await了个请求,这个协程就像同步代码一样暂停执行,然后协程的事件循环调度系统就会去寻找已经获得请求响应的协程,获得响应的协程就激活继续执行。
这种协程模式比纯粹的多线程开销要小,而且知道每个协程确切运行状态,缺点是代码比多线程啰嗦,async/await有侵入性和传染性。
谢谢大家,好像有点理解了,试了下join! 确实在单线程模式下也不会出现一个Future必须等另一个Future结束的情况。
现在问题是不知道如何像join!一样在单线程里同时(concurrently)让多个Future开始执行,比如join(sleep(1), sleep(2)).await 实际总共只会睡2秒而不是1秒
看了下源码也还看不懂,后面可能要再看下
楼下songzhi的回答基本描述清楚了的,await是针对当前task的,它实际的作用是告诉Runtime,我现在要等某个条件,您可以先去运行其他task。眼光升高些,从Runtime的角度看,一下就理解了。
要实现js的那种,可以用join!()或者spawn
你首先要明白一个目标,为什么要使用async await这种语法。 一句话,这种语法的目的就是让你用同步的方式去写异步代码,所以你看起来像是同步才是目的。 异步代码之所以不好写,就是因为它等于要在异步等待的时候让出CPU给其他程序,传统方式经常实现成回调或者轮询+跳转等方式,代码难管理,而同步代码是好管理的,所以我们要让异步代码看起来像同步。 await确实是等待,但是它只是让当前这个task(或者说协程)等待,而一个程序内有很多task,这个等待了CPU可以去处理其他的task,等你这个等待结束了再调用,就有点像线程阻塞后,操作系统会调度其他线程继续执行一样,这样你线程里运行的代码就可以使用同步阻塞而不用怕浪费CPU,没有额外的心智负担。
单线程也可以spawn哦
--
👇
ZZG: spawn 去获得一个JoinHandle,必须是tokio 多线程模式才可以先打印"hello",再运行async_fn1和async_fn2,但是用多线程去运行感觉有点耍赖...因为不用tokio不用异步,单用std::thread也可以达到先打印"hello"的效果。
如果是tokio current_thread模式,还是只能先执行async_fn1,然后async_fn2,然后"hello"。这也才是我最关心的,在同一个线程下,或者说在一段不包括spawn的简单异步代码中,其运行是不是相当于完全同步的?那么在这种情况下,异步的好处在哪里?
--
👇
wwwwwwwwww: 按你这例子,其实对应的是 https://docs.rs/tokio/latest/tokio/task/struct.JoinHandle.html 至于为什么不立即执行,可以看下具体异步的实现机制 https://rustcc.cn/article?id=f3c2da66-aa60-46e3-9a0b-cd208d136e86 那么为什么被设计为不立即执行,个人理解是这样为具体逻辑留下了更多空间(比如函数返回一个Future,再按需调度),而且避免了与Runtime的高度耦合
Rust 里面 async/.await 是一回事,底层的 Future 又是另一回事,消费一个 Future 并不必须要 .await,你还可以手动 poll 它。当你同时 poll 多个 Future 的时候,就可以说你在并发(并发不是并行)执行这几个 Future 了,对应的 tokio 中封装好的 macro 是 join 和 select。.await 的好处是你可以用同步的方式写出异步的代码,这在大多数情况下是很方便的。
说说我的浅薄理解,尽量不涉及底层原理。
明白rust async模型,要建立在async解决了什么问题的角度上思考,也要明白await在rust这套模型中意味着什么。
async绝大部分的使用场景是读写socket。读写socket属于慢速系统调用的一种,在多路复用(select epoll)诞生前想要并行的处理多个连接需要使用多线程才能实现。
多路复用诞生后一个线程就可以处理大量的连接,但是以往的“顺序代码”势必要进行多路复用重构,加大了编写难度以及心理负担。
协程模型就是可以使用以往的顺序代码的方式辅以语言特性 多路复用来达到开发者有好的代码编写体验。
对于每个runtime spawn出来的task,当这个task执行到await点的时候都是在告诉runtime:我需要读/写某个资源,倘若这个资源无效的话,那么当这个资源有效后在唤醒我,现在可以将我调度出去运行下一个task。当然不是说所有async函数的await点都是这样。
随后当某个时刻,假设是runtime没有可运行的task了,将会执行多路复用代码,来等待每个task在await点中注册的资源有效,当一个或多个资源有效后,将这些资源对应的task转为可运行状态继续运行。
你可能理解错了异步的好处了,异步主要是在等io(比如网络请求)时,当前线程可以滚去做其他事情。有兴趣的话可以看看nodejs,默认就是单线程异步的。
按你这个例子,想要你说的效果,可以试试用tokio提供的异步sleep哦,那样会导致异步任务切换,正常就可以看到你要的效果了
--
👇
ZZG: spawn 去获得一个JoinHandle,必须是tokio 多线程模式才可以先打印"hello",再运行async_fn1和async_fn2,但是用多线程去运行感觉有点耍赖...因为不用tokio不用异步,单用std::thread也可以达到先打印"hello"的效果。
如果是tokio current_thread模式,还是只能先执行async_fn1,然后async_fn2,然后"hello"。这也才是我最关心的,在同一个线程下,或者说在一段不包括spawn的简单异步代码中,其运行是不是相当于完全同步的?那么在这种情况下,异步的好处在哪里?
--
👇
wwwwwwwwww: 按你这例子,其实对应的是 https://docs.rs/tokio/latest/tokio/task/struct.JoinHandle.html 至于为什么不立即执行,可以看下具体异步的实现机制 https://rustcc.cn/article?id=f3c2da66-aa60-46e3-9a0b-cd208d136e86 那么为什么被设计为不立即执行,个人理解是这样为具体逻辑留下了更多空间(比如函数返回一个Future,再按需调度),而且避免了与Runtime的高度耦合
spawn 去获得一个JoinHandle,必须是tokio 多线程模式才可以先打印"hello",再运行async_fn1和async_fn2,但是用多线程去运行感觉有点耍赖...因为不用tokio不用异步,单用std::thread也可以达到先打印"hello"的效果。
如果是tokio current_thread模式,还是只能先执行async_fn1,然后async_fn2,然后"hello"。这也才是我最关心的,在同一个线程下,或者说在一段不包括spawn的简单异步代码中,其运行是不是相当于完全同步的?那么在这种情况下,异步的好处在哪里?
--
👇
wwwwwwwwww: 按你这例子,其实对应的是 https://docs.rs/tokio/latest/tokio/task/struct.JoinHandle.html 至于为什么不立即执行,可以看下具体异步的实现机制 https://rustcc.cn/article?id=f3c2da66-aa60-46e3-9a0b-cd208d136e86 那么为什么被设计为不立即执行,个人理解是这样为具体逻辑留下了更多空间(比如函数返回一个Future,再按需调度),而且避免了与Runtime的高度耦合
按你这例子,其实对应的是 https://docs.rs/tokio/latest/tokio/task/struct.JoinHandle.html 至于为什么不立即执行,可以看下具体异步的实现机制 https://rustcc.cn/article?id=f3c2da66-aa60-46e3-9a0b-cd208d136e86 那么为什么被设计为不立即执行,个人理解是这样为具体逻辑留下了更多空间(比如函数返回一个Future,再按需调度),而且避免了与Runtime的高度耦合