async/await 编程理解
博客主要是参考 Asynchronous Programming in Rust ,会结合简单的例子,对 async 和 await 做比较系统的理解,如何使用 async 和 await 是本节的重点。
async 和 await 主要用来写异步代码,async 声明的代码块实现了 Future 特性,如果实现 Future 的代码发生阻塞,会让出当前线程的控制权,允许线程去执行别的 Future 代码。
我把 async 代码块使用 Future 来表示,关键是理解 future 和执行线程之间的关系。系统中存在多个 future时,多个 future 的执行顺序如何控制?以及多个 future 如何同时并发执行?文章会尝试解释这些问题。

多个 async 串行执行
在 Cargo.toml 中增加如下依赖。
[dependencies]
futures = "0.3.28"
使用 async fn 创建异步方法,关键字 async 还可以用来声明代码块,在闭包函数中可能会见到。
async fn do_something() { /* ... */ }
我对原始内容的例子做了简化,来探究异步执行的过程。仍然声明了三个 async 方法,分别对应「学习唱歌」、「唱歌」、「跳舞」三个函数,假设这三个函数可以并行异步执行,那么这三个过程的顺序便是随机的、不确定的。
use futures::executor::block_on;async fn learn_song() { println!("learn song");
}async fn sing_song() { println!("sing song");
}async fn dance() {println!("dance");
}async fn async_main() {let f1 = learn_song();let f2 = sing_song();let f3 = dance();futures::join!(f1, f2, f3);
}fn main() {block_on(async_main());
}
本地执行的结果是,上述代码无论执行多少次,输出都是确定的。理论上确实不应该,我都有点怀疑:会不会代码太简单导致的。
修改一下 learn_song 函数,在打印之前执行多次 for 循环。如果这几个过程是并发的,那么 learn_song 肯定是最后被执行完成的。遗憾的是,控制台第一个输出的还是 “learn song”。这也说明,上述代码并没有被并发执行。
async fn learn_song() { let mut i = 0;while i < 10000 {i = i + 1;}println!("learn song");
}
原文代码示例
问题究竟出在哪里了呢?重新看一下原文章的demo,试着对比一下哪里出了问题。
async fn learn_and_sing() {// Wait until the song has been learned before singing it.// We use `.await` here rather than `block_on` to prevent blocking the// thread, which makes it possible to `dance` at the same time.let song = learn_song().await;sing_song(song).await;
}async fn async_main() {let f1 = learn_and_sing();let f2 = dance();// `join!` is like `.await` but can wait for multiple futures concurrently.// If we're temporarily blocked in the `learn_and_sing` future, the `dance`// future will take over the current thread. If `dance` becomes blocked,// `learn_and_sing` can take back over. If both futures are blocked, then// `async_main` is blocked and will yield to the executor.futures::join!(f1, f2);
}fn main() {block_on(async_main());
}
join!和await类似,只是join!能够等待多个并发执行的 future,如果代码被临时阻塞在learn_and_sing,dance会被当前线程接管。如果dance被阻塞,learn_and_sing会重新被接管。如果两个方法同时被阻塞,async_main就会被阻塞,会使当前执行阻塞。
通过代码注释,我抠到一个字眼: the current thread,这难道说明函数 f1 和 f2 是在一个线程上执行的?如果它们是在一个线程上执行的,又因为方法体内部都没有类似网络IO的阻塞,那确实有可能导致串行执行的效果。
rust 对 async 的支持
重新翻看语言和库的支持 Language and library support介绍,我决定把中心放在 async 的设计实现上,async 在底层是如何被处理的。
- 最基础的特性、类型和方法,比如 Future 特性都被标准库提供
- async/await 语法直接被 Rust 编译器支持
- futures 库提供了一些工具类型、宏、函数,它们可以被使用在 Rust 应用程序中
async runtimes比如 Takio、async-std,提供了执行一部代码、IO、创建任务的能力。大部分的异步应用和异步库都依赖具体的运行时,可以查看 The Async Ecosystem 了解细节
关键点在异步运行时上,原来 rust 只提供了一套异步运行时的接入标准,具体的实现则是由第三方库自己决定的,比较有名的的运行时三方库包括 Takio 和 async-std。不得不说,Rust 设计的眼界确实比较高。
我想通过 Single Threaded vs Multi-Threaded Executors 来理解单线程和多线程的执行。前面的例子中,我们有怀疑:代码没有并行是因为在单线程中执行导致的。当然,也可能是因为我们没有明确指定 async runtimes。
异步的 executor 可以是单线程、也可以是多线程执行,async-executor 库就同时提供了这两种 executor
.
多线程 executor 可以同时处理多个任务,这会大大加快了处理速度。但任务之间的数据同步成本也会更高一些。建议通过性能测试来决策选择单线程还是多线程
.
任务既可以被创建它的线程执行,也可以被其他独立线程执行。即使任务被独立线程执行,其运行也是非阻塞的。如果想要调度任务在多线程上执行,任务本身必须要实现 Send 特性。有的运行时还支持创建 non-Send 任务,用来保证每个任务都被创建它的线程执行。有的库中还支持将生成的阻塞任务交给特定的线程去执行,这对于运行阻塞代码会非常有用
看到这里,我决定指定 Takio 运行时来重新运行上述代码,如何指定呢?
指定 Takio 运行时
takio 的官网挺花里胡哨,大大的标题:Build reliable network applications without compromising speed. 直译过来是构建可靠的网络应用而不向降低运行效率妥协。不过理性提醒我:这种既要又要的声明,应该得有前提吧。
takio 中的例子是 redis 的写入和读取过程,我要介绍的内容都基于 Hello Tokio 示例的解释,只不过,解释的顺序上会有一点点调整。
如何开启异步运行时
async fn必须被异步运行时执行,运行时包含异步任务调度、IO事件、计时器等。但这个运行时并不会自动开启,我们可以通过 main 函数来开启它。
.
#[tokio::main]宏可以将async fn main()转换为同步的fn main(),并为它初始化了运行时实例,然后执行异步的 async main 函数

在我的例子中,main 函数最后也调用了 block_on 函数,这里调用的是 rt.block_on 方法,不晓得两者是否存在差异?不过,后文有必要等会去看看,在没有指定运行时的情况下 block_on 的执行效果。
Cargo features
引入 tokio 依赖,其中的 features 属性可以指定引入的范围。现在,我们使用 full 来标识导入所有特性。不过,我感觉引入 tokio 会和例子中的 futures 发生冲突,引入 tokio 后,明显就不需要引入 futures::executor::block_on 了。
还是得继续看看测试效果,用上面的例子来验证一下
[dependencies]
tokio = { version = "1", features = ["full"] }
异步程序
对于同步程序来说,如果程序执行过程中遇到阻塞操作,它就会阻塞在那里直到程序处理完成。拿建立 TCP 连接来说,需要建连的两端通过网络进行数据交互,但这个过程会花费相当多的时间,线程就只能阻塞,直到建连完成。
.
对于异步程序来说,不能立即完成的操作会被系统挂起到后台,当前线程并不会被阻塞,它仍可以继续执行别的任务。一旦被挂起的任务恢复,它便可以从上次执行的位置重新开始执行。因为我们的例子中只有一个任务,因此当操作被挂起时,并没有触发别的任务执行,但异步程序通常是会包含多个任务的。
关于异步的解释,这里特意提到了任务:task。只有在多个 task 的情况下,异步执行才有意义。上文还一直提到的 executor、recator,它们都算得上异步运行时的关键模块。
我忍不住思考,最初例子中的「学习唱歌」、「唱歌」、「跳舞」是属于三个独立的任务吧,应该是的。
Compile-time green-threading
green-threading 阅读到这里的小伙伴,欢迎留言来解释,它的具体含义。点击链接可以直接跳转到对应的原文。
异步回归
首先引入 tokio 的包,其次,使用 #[tokio::main] 来标识开启异步运行时,删除之前的 main 函数,简单替换成下面的 main 函数声明。非常遗憾,下面的代码仍然无法异步执行。
#[tokio::main]
async fn main() {let f1 = learn_song();let f2 = sing_song();let f3 = dance();futures::join!(f1, f2, f3);
}
难不成是因为 task ,我们现在看看 tokio 中对 task 的用法,现在的代码只是有了 tokio 的面,并没有 tokio 的里。
task
tokio task 是一个异步的 green 线程,需要通过
tokio::spawn方法进行创建,该方法返回JoinHandle类型对象,调用者需要通过这个返回值来和该 task 进行交互。如果 task 包含返回值,调用者可以链式对JoinHandle调用await来获取。
结合下面的例子,看起来 .await 是用来阻塞等待 task 执行完成的。启动一个 task 之后,必须确保 task 执行完成,才能继续执行后续的依赖流程,有点类似 java 的异步线程。
#[tokio::main]
async fn main() {let handle = tokio::spawn(async {// Do some async work"return value"});// Do some other worklet out = handle.await.unwrap();println!("GOT {}", out);
}
在 JoinHandle 上调用 await 会返回一个 Result 对象。如果 task 在执行期间发生错误,JoinHandle 会返回一个 Err 类型。当 task 发生 panic,或者被运行时强制取消时便会触发这个 Err。
task 是运行时调度的基本执行单元,task 创建后会被提交给 tokio 调度器,我们只需要在使用的地方等待 task 执行完成即可。task 可能会被创建它的线程执行,也可能会交给别的运行时线程执行,总之,可以在不同的线程上被执行。
在 tokio 中 task 是非常轻量的,它们只需要单个分配和64字节的内存。
和 go 语言比较的话,task 相当于 GMP 调度中的 G 对象,也是一个非常轻量的对象,可以在不同的线程中被调用。
使用 tokio 来重构
上面也提到过,现在的代码只有 tokio 的表,没有 tokio 的里,现在我们重新使用 tokio 异步来实现一遍。通过简单的改造,终于实现了异步执行的效果,不禁感慨,确实不易啊。
fn learn_song() { println!("learn song");
}fn sing_song() { println!("sing song");
}async fn dance() {println!("dance");
}#[tokio::main]
async fn main() {let f1 = tokio::spawn(async {learn_song();});let f2 = tokio::spawn(async {sing_song();});let f3 = tokio::spawn( dance());futures::join!(f1, f2, f3);
}
回顾
rust 异步调用的运行时是依赖三方实现的,在不引入 tokio 的情况下,并不会实现并发。同时,rust 中 tokio 的设计也类似于 go 语言中 GMP 的设计,异曲同工。
相关文章:
async/await 编程理解
博客主要是参考 Asynchronous Programming in Rust ,会结合简单的例子,对 async 和 await 做比较系统的理解,如何使用 async 和 await 是本节的重点。 async 和 await 主要用来写异步代码,async 声明的代码块实现了 Future 特性&a…...
1. Java基础知识介绍
文章目录 1. Java基础知识介绍1. 简单输入输出1.1 输入1.2 输出 2. 数据类型2.1 基本数据类型2.2 引用数据类型 3. 函数4. 类基础知识5. 小结 1. Java基础知识介绍 1. 简单输入输出 在Java编程中,我们经常需要与用户进行交互,以获取输入并输出结果。Ja…...
基础shell小技巧01
1. 命令替换 shell脚本中最有用的特性之一是可以从命令输出中提取信息并将其赋给变量。把输出赋给变量之后,就可以随意在脚本中使用了。在脚本中处理数据时,这个特性显得尤为方便。 有两种方法可以将命令输出赋给变量。 反引号(࿰…...
微信小程序--data的赋值与取值的几种方式
通过小程序官方文档可知: 赋值一定需要注意。需要setData的使用,这样页面才刷新,数据才会改变,并且分清that和this的使用 Page() 函数用来注册一个页面。接受一个 object 参数,其指定页面的初始数据、生命周期函数、事…...
条码通讯配置
由Leuze(劳易测) LSIS 222 M5M-R1/MA208i系统组成 连接说明及器件明细: 打开条码调试软件BPS Configuration tool对M208i进行ip 和端口号进行配置,条码选择作为客户端,输入ip和端口号,选择串口通讯波特…...
知网G4期刊《高考》简介及投稿要求
知网G4期刊《高考》简介及投稿要求 一、《高考》期刊简介: 主管单位:长春市委宣传部 主办单位:长春出版社 国内刊号22-1372/G4 国际刊号1673-6265 代号12-240 编辑单位:《高考》杂志社 出版周期:旬刊 类 …...
第7章 CPU前端优化
接下来讨论如何使用CPU监控特性寻找CPU上运行的代码中可被调优的位置。 标准的算法和数据结构在性能敏感型工作负载并不总能表现的很好。例如,在“扁平化”数据结构的冲击下,链表基本上快被放弃了。传统链表中的每个节点都是动态分配的,除了…...
idea新建Java-maven项目时,出现Dependency‘xxx(jar包名)‘ not found的解决方案
项目场景: 项目场景:使用idea创建maven项目时,导入简单依赖时(本文以mysql-connector-java为例)。 问题描述 问题: 首先,在创建新的maven项目中,出现下列两种情况: &am…...
STM32--USART串口
文章目录 通信接口串口通信硬件电路电平标准参数时序 USART主要特性框图 数据帧发送器 波特率发生器SWART串口发送与接收工程串口收发数据包 通信接口 通信接口是指连接中央处理器(CPU)和标准通信子系统之间的接口,用于实现数据和控制信息在不…...
2023年Java毕业设计题目推荐,怎样选题?500道毕业设计题目推荐
大家好,我是程序员徐师兄,最近有很多同学咨询,说毕业设计了,不知道选怎么题目好,有哪些是想需要注意的。 今天,我整理了一些Java毕业设计的题目,可以参考一下,希望对大家有所帮助 文章目录 一、…...
基于数据湖的多流拼接方案-HUDI概念篇
目录 一、为什么需要HUDI? 1. 传统技术选型存在哪些问题? 2. Hudi有什么优点? 基于 Hudi Payload 机制的多流拼接方案: 二、HUDI的应用场景 1. 什么场景适合使用hudi? 2. 什么场景不适合使用hudi? …...
OpenCV基础知识(5)— 几何变换
前言:Hello大家好,我是小哥谈。OpenCV中的几何变换是指改变图像的几何结构,例如大小、角度和形状等,让图像呈现出缩放、翻转、旋转和透视效果。这些几何变换操作都涉及复杂、精密的计算。OpenCV将这些计算过程都封装成了非常灵活的…...
Linux下源码安装MySQL 8.0
MySQL 8.0源码安装 环境准备步骤 环境准备 Linux环境,本文基于CentOS 8 MySQL安装包,本文基于MySQL 8.1,以下为带boost MySQL 8.1源码下载地址: https://dev.mysql.com/get/Downloads/MySQL-8.1/mysql-boost-8.1.0.tar.gz 步骤…...
大聪明教你学Java | 深入浅出聊 Java 内存模型
前言 🍊作者简介: 不肯过江东丶,一个来自二线城市的程序员,致力于用“猥琐”办法解决繁琐问题,让复杂的问题变得通俗易懂。 🍊支持作者: 点赞👍、关注💖、留言💌~ 在多线程环境下,多个线程同时访问共享数据可能导致一系列问题,如数据不一致、竞态条件和死锁等…...
SAP ABAPG开发屏幕自动生成日期的搜索帮助
代码如下: REPORT z_jason_test_f4 . TABLES: s031. PARAMETER p_spmon TYPE spmon DEFAULT sy-datum0(6) OBLIGATORY. SELECT-OPTIONS s_spmon FOR s031-spmon DEFAULT sy-datum0(6) OBLIGATORY. AT SELECTION-SCREEN ON VALUE-REQUEST…...
leetcode 674. 最长连续递增序列
2023.8.24 与最长递增子序列 类似,不同的是, 本题要求连续序列,所以不需要第二层遍历比较之前所有的元素了,只需要比较上一个元素i-1。 dp[i]的含义为:以nums[i]元素为结尾的序列的最长递增子序列。 注意这里是以i为结…...
Mysql简短又易懂
MySql 连接池:的两个参数 最大连接数:可以同时发起的最大连接数 单次最大数据报文:接受数据报文的最大长度 数据库如何存储数据 存储引擎: InnoDB:通过执行器对内存和磁盘的数据进行写入和读出 优化SQL语句innoDB会把需要写入或者更新的数…...
vue 简单实验 v-model 变量和htm值双向绑定
1.代码 <script src"https://unpkg.com/vuenext" rel"external nofollow" ></script> <div id"two-way-binding"><p>{{ message }}</p><input v-model"message" /> </div> <script>…...
测试框架pytest教程(8)失败重试-pytest-rerunfailures
pytest-rerunfailures是一个pytest插件,用于重新运行失败的测试用例。当测试用例在第一次运行时失败,该插件会自动重新运行指定次数的失败用例,以提高稳定性和减少偶发性错误的影响。 要使用pytest-rerunfailures插件,需要按照以…...
6个主流的工业3D管道设计软件
3D 管道设计软件是大多数行业工程工作的主要部分,例如: 电力、石油和天然气、石化、炼油厂、纸浆和造纸、化学品和加工业。 全球各工程公司使用了近 50 种工厂或管道设计软件。 每个软件都有优点和缺点,包括价格点。 EPC 和业主部门当前的趋势…...
安宝特方案丨XRSOP人员作业标准化管理平台:AR智慧点检验收套件
在选煤厂、化工厂、钢铁厂等过程生产型企业,其生产设备的运行效率和非计划停机对工业制造效益有较大影响。 随着企业自动化和智能化建设的推进,需提前预防假检、错检、漏检,推动智慧生产运维系统数据的流动和现场赋能应用。同时,…...
高频面试之3Zookeeper
高频面试之3Zookeeper 文章目录 高频面试之3Zookeeper3.1 常用命令3.2 选举机制3.3 Zookeeper符合法则中哪两个?3.4 Zookeeper脑裂3.5 Zookeeper用来干嘛了 3.1 常用命令 ls、get、create、delete、deleteall3.2 选举机制 半数机制(过半机制࿰…...
2025 后端自学UNIAPP【项目实战:旅游项目】6、我的收藏页面
代码框架视图 1、先添加一个获取收藏景点的列表请求 【在文件my_api.js文件中添加】 // 引入公共的请求封装 import http from ./my_http.js// 登录接口(适配服务端返回 Token) export const login async (code, avatar) > {const res await http…...
学习STC51单片机32(芯片为STC89C52RCRC)OLED显示屏2
每日一言 今天的每一份坚持,都是在为未来积攒底气。 案例:OLED显示一个A 这边观察到一个点,怎么雪花了就是都是乱七八糟的占满了屏幕。。 解释 : 如果代码里信号切换太快(比如 SDA 刚变,SCL 立刻变&#…...
.Net Framework 4/C# 关键字(非常用,持续更新...)
一、is 关键字 is 关键字用于检查对象是否于给定类型兼容,如果兼容将返回 true,如果不兼容则返回 false,在进行类型转换前,可以先使用 is 关键字判断对象是否与指定类型兼容,如果兼容才进行转换,这样的转换是安全的。 例如有:首先创建一个字符串对象,然后将字符串对象隐…...
管理学院权限管理系统开发总结
文章目录 🎓 管理学院权限管理系统开发总结 - 现代化Web应用实践之路📝 项目概述🏗️ 技术架构设计后端技术栈前端技术栈 💡 核心功能特性1. 用户管理模块2. 权限管理系统3. 统计报表功能4. 用户体验优化 🗄️ 数据库设…...
深度学习水论文:mamba+图像增强
🧀当前视觉领域对高效长序列建模需求激增,对Mamba图像增强这方向的研究自然也逐渐火热。原因在于其高效长程建模,以及动态计算优势,在图像质量提升和细节恢复方面有难以替代的作用。 🧀因此短时间内,就有不…...
STM32HAL库USART源代码解析及应用
STM32HAL库USART源代码解析 前言STM32CubeIDE配置串口USART和UART的选择使用模式参数设置GPIO配置DMA配置中断配置硬件流控制使能生成代码解析和使用方法串口初始化__UART_HandleTypeDef结构体浅析HAL库代码实际使用方法使用轮询方式发送使用轮询方式接收使用中断方式发送使用中…...
计算机基础知识解析:从应用到架构的全面拆解
目录 前言 1、 计算机的应用领域:无处不在的数字助手 2、 计算机的进化史:从算盘到量子计算 3、计算机的分类:不止 “台式机和笔记本” 4、计算机的组件:硬件与软件的协同 4.1 硬件:五大核心部件 4.2 软件&#…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...
