一文读懂Rust的async

不同的编程语言表现异步编程的方式可能不一样,Rust跟JavaScript的async/await类似:使用的关键字啊,编程模型啊都差不多啦! 也有些不一样的地方,毕竟Rust是重新设计的语言嘛,比如:在JavaScript中使用Promise表示需要延迟异步执行的计算,在Rust中使用的是Future.在JavaScript中不需要选择指定运行异步代码的运行时,在Rust中需要. Rust还更麻烦了?还得选择指定运行时?

这是因为Rust是可以面向硬件、嵌入式设备的操作系统级别的编程语言就像C++,并且零抽象成本.这就需要Rust需要有选择地把功能包含进标准库里.简单来说,为了满足不同的编程场景Rust标准库就没有包含指定异步代码运行时,我们可以根据具体的场景选择不同的运行时。

是不是感觉还有点晕乎?没关系,下面我们会介绍怎么在Rust中编写异步(async)代码.知道了怎么编写异步代码,也就知道async是什么了.如果你是第一次使用Rust编写异步代码或者第一次使用异步代码库正在迷茫从何入手,那恭喜你,这篇文档特别适合你.开始之前我们先快速的介绍下异步编程的基本要素.

基本要素

编写异步的应用,至少需要俩个crate:

  1. futures:这个是Rust官方团队提供维护的crate.
  2. 异步代码运行时crate: 可以自己选择,比如:Tokio, async_std, smol等等.

你可能不想在项目中引入过多依赖,但这些依赖就像chronolog是比较基础的依赖.唯一的不同是这些依赖是面向异步编程的.

我们接下来会使用Tokio做为运行时,刚开始你最好也先了解熟悉使用一种运行时,然后再尝试使用其它运行时。

因为这些运行时之间有很多相通的地方,熟悉了一个再去熟悉其它的就简单了。就像我们学习编程语言一样,学好学深一门编程语言,再去学习其它的语言就快了。不要一开始就几门语言一起学,这样很可能实际开发时这也不行那也不行换来换去还是不能开发出东西.

我们可以像下面这样引入依赖:

[dependencies]
futures = { version = "0.3.*" }
tokio = {version = "0.2.*", features = ["full"] }

main.rs中敲入以下代码:

use futures::prelude::*;
use tokio::prelude::*;fn main() {todo!();
}

可以执行下cargo check如果没什么报错信息,依赖配置就完成了.接下来我们介绍怎么使用运行时。

运行时

像我们先前说的Rust标准库并没有指定异步代码的运行时,所以我们自己选择运行时并配置相应的依赖。这里我们选择了使用Tokio:

tokio = {version = “0.2.*”, features = [“full”] }

有些第三方库可能需要我们使用指定的异步代码运行时,因为它们内部是对特定运行时库进行了封装。比如:web开发框架actix_web就是基于tokio封装开发的.但大多少情况我们都可以自己选择运行时。无论我们选择那一种运行时,在开始编写代码前都需要先搞清除:

  1. 怎么启动运行时?
  2. 怎么生成 Future
  3. 怎么处理阻塞(IO密集)和CPU密集任务?

搞清除了这三个问题基本上也就学会怎么编写异步代码了.接下来我们就以tokio为例演示下:

  1. 启动运行时

    可以实例化一个运行时,并派生一个Future指定给运行时。这个Future就是异步代码的主入口,可以把它想象成异步代码的main函数:

    async fn app() {todo!()
    }fn main() {let mut rt = tokio::runtime::Runtime::new().unwrap();let future = app();rt.block_on(future);
    }
    

    还可以使用宏,简化代码为:

    #[tokio::main]
    async fn main() {}
    

    虽然代码行数少了,功能跟上面的代码还是一样的哦!

  2. 为运行时生成Future

    你想并发运行多个任务时,就可以像这样生成Future:

    use tokio::task;async fn our_async_program() {todo!();
    }async fn app() {let concurrent_future = task::spawn(our_async_program());todo!()
    }
    
  3. 处理阻塞和CPU密集性任务

    什么是阻塞性的任务?什么是CPU密集性的任务呢?可以简单的理解为这两种任务都会长时间的霸占CPU阻塞线程继续执行其它任务.就好比工地上有个包工头专门负责分配任务给小工门干,有些小活小任务包工头可能顺手就干了,但是一些耗时比较长的比如去搬一车砖头,包工头就不能自己去干了,因为它去搬砖头了就没人负责任务分配了,小工们活都干完了只能等着包工头分配任务才能继续干活.包工头呢?还在搬砖头呢.显然这是会影响整体工作效率的。代码也一样,要有个专门负责总体分配任务的线程,在这个线程中就不能再执行其它比较耗费时间的的任务了。那耗费时间的任务谁来执行呢?小工呗,也就是派生新的Future. 就像这个样子:

    use tokio::task;fn fib_cpu_intensive(n: u32) -> u32 {match n {0 => 0,1 => 1,n => fib_cpu_intensive(n - 1) + fib_cpu_intensive(n - 2),}
    }async fn app() {let threadpool_future = task::spawn_blocking(||fib_cpu_intensive(30));todo!()
    }
    

    tokio是使用的spawn_blocking去派生新的Future使用新的线程执行比较耗时的任务,其它运行时库可能API不一样但也会提供类似的方法.

异步开发样例

支持我们已经学习了怎么使用Rust编写异步代码,接下来把所学内容整合到一起做个样例:

use futures::prelude::*;
use tokio::prelude::*;
use tokio::task;
use log::*;// Just a generic Result type to ease error handling for us. Errors in multithreaded
// async contexts needs some extra restrictions
type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;async fn app() -> Result<()> {// I treat this as the `main` function of the async part of our program. todo!()
}fn main() {env_logger::init();let mut rt = tokio::runtime::Runtime::new().unwrap();match rt.block_on(app()) {Ok(_) => info!("Done"),Err(e) => error!("An error ocurred: {}", e),};
}

使用到的crate有:

  • 提供异步代码运行时的 tokio
  • Rust日志门面log
  • 日志工具env_logger

Cargo.toml类似这个样子:

[dependencies]
futures = { version = "0.3.*"}
tokio = {version = "0.2.*", features = ["full"] }
log = "0.4.*"
env_logger = "0.7.*"

需要注意的是env_logger需要根据环境变量RUST_LOG设置日志级别

基本上所有的异步编程项目都可以使用类似这样的依赖配置和main.rs.根据不同的使用场景还可以优化下错误处理和日志.比如可以考虑使用 Anyhow处理错误,可以考虑使用 async-log更好的在异步多线程环境中输出日志.在本文档中接下来的代码就基于这个样例模板开发了。

异步函数

在Rust中编写异步函数跟先前编写普通函数有点不一样.先前接触Rust函数时,你可能已经注意到函数的参数返回值都需要声明确切的类型。异步函数的返回值都是经过Future包装的。如果你读了关于Future的文档,按照这个思路你可能认为应该像下面这样编写异步函数:

async fn our_async_program() -> impl Future<Output = Result<String>> {future::ok("Hello world".to_string()).await
}

不用这么麻烦,比较Rust是重新设计的语言.当你使用async关键字时,Rust会自动的使用Future封装返回只,所以你原来怎么给普通函数定义返回值就继续那么地干,就像这个样子:

async fn our_async_program() -> Result<String> {future::ok("Hello world".to_string()).await
}

这里使用的future::ok是future库提供的方便我们开发使用的,用于生成状态为readyfuture.

你可能会见到使用异步代码块async {...}创建异步代码的,这是为了更灵活的定义返回值类型,不过大多少情况下使用异步函数就够了.接下来我们编写一个使用异步函数的例子.

创建Web请求

在Rust中futurelazy(懒)的.也就是说,默认情况下当你创建了一个future,它是什么都不干的,非得等你调用await告诉它该干活了,它才开始干活.

接下来我们以发起处理Web请求的场景用代码演示一下子:

fn slowwly(delay_ms: u32) -> reqwest::Url {let url = format!("http://slowwly.robertomurray.co.uk/delay/{}/url/http://www.google.co.uk", delay_ms,);reqwest::Url::parse(&url).unwrap()
}async fn app() -> Result<()> {info!("Starting program!");let _resp1 = reqwest::get(slowwly(1000)).await?;info!("Got response 1");let _resp2 = reqwest::get(slowwly(1000)).await?;info!("Got response 2");Ok(())
}

创建web请求使用到了reqwest库,需要把这个库添加到Cargo.toml的依赖区域:

reqwest = “0.10.*”

执行上面的代码输出类似这个样子:

1.264 [INFO] - Got response 1
2.467 [INFO] - Got response 2
2.468 [INFO] - Done

这里的日志格式是自定义的,前面的数字是程序执行的时间,自定义日志格式的代码是这个样子地:

et start = std::time::Instant::now();
env_logger::Builder::from_default_env().format(move |buf, rec| {let t = start.elapsed().as_secs_f32();writeln!(buf, "{:.03} [{}] - {}", t, rec.level(),rec.args())
}).init();

从日志输出可以看出,我们的函数并不是一起执行的,而是一个执行完成后另一个才开始执行的,因为我们这里还是使用的普通函数并没有使用异步函数.接下来是修改为异步函数的版本:

async fn request(n: usize) -> Result<()> {reqwest::get(slowwly(1000)).await?;info!("Got response {}", n);Ok(())
}async fn app() -> Result<()> {let resp1 = task::spawn(request(1));let resp2 = task::spawn(request(2));let _ = resp1.await??;let _ = resp2.await??;Ok(())
}

tokio提供的spawn函数可以让我们使用多线程并发执行异步函数.

执行的效果类似这个样子:

1.247 [INFO] - Got response 2
1.256 [INFO] - Got response 1
1.257 [INFO] - Done

可以跟上面使用普通函数的方式对比一下子,是不是总体效率快多了,俩个请求不需要互相等待,各自说干就干,就是这么快.

补充

什么时候该派生Future执行任务呢?这里有几条建议

  1. 优先选用没有阻塞的操作库.
  2. 如果不确定就派生一个吧.

学员专享pdf版本请点这里下载

参考原文

一文读懂Rust的async相关推荐

  1. 一文读懂SpringBoot中的事件机制

    一文读懂SpringBoot中的事件机制?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法. 要"监听"事件,我们总是 ...

  2. 从实验室走向大众,一文读懂Nanopore测序技术的发展及应用

    关键词/Nanopore测序技术    文/基因慧 随着基因测序技术不断突破,二代测序的发展也将基因检测成本大幅降低.理想的测序方法,是对原始DNA模板进行直接.准确的测序,消除PCR扩增带来的偏差, ...

  3. 一文读懂Faster RCNN

    来源:信息网络工程研究中心本文约7500字,建议阅读10+分钟 本文从四个切入点为你介绍Faster R-CNN网络. 经过R-CNN和Fast RCNN的积淀,Ross B. Girshick在20 ...

  4. 福利 | 一文读懂系列文章精选集发布啦!

    大数据时代已经悄然到来,越来越多的人希望学习一定的数据思维和技能来武装自己,虽然各种介绍大数据技术的文章每天都扑面而来,但纷繁又零散的知识常常让我们不知该从何入手:同时,为了感谢和回馈读者朋友对数据派 ...

  5. ​一文读懂EfficientDet

    一文读懂EfficientDet. 今年年初Google Brain团队在 CVPR 2020 上发布了 EfficientDet目标检测模型, EfficientDet是一系列可扩展的高效的目标检测 ...

  6. 一文读懂序列建模(deeplearning.ai)之序列模型与注意力机制

    https://www.toutiao.com/a6663809864260649485/ 作者:Pulkit Sharma,2019年1月21日 翻译:陈之炎 校对:丁楠雅 本文约11000字,建议 ...

  7. AI洞观 | 一文读懂英特尔的AI之路

    AI洞观 | 一文读懂英特尔的AI之路 https://mp.weixin.qq.com/s/E9NqeywzQ4H2XCFFOFcKXw 11月13日-14日,英特尔人工智能大会(AIDC)在北京召 ...

  8. 一文读懂机器学习中的模型偏差

    一文读懂机器学习中的模型偏差 http://blog.sina.com.cn/s/blog_cfa68e330102yz2c.html 在人工智能(AI)和机器学习(ML)领域,将预测模型参与决策过程 ...

  9. 一文读懂AI简史:当年各国烧钱许下的愿,有些至今仍未实现

    一文读懂AI简史:当年各国烧钱许下的愿,有些至今仍未实现 导读:近日,马云.马化腾.李彦宏等互联网大佬纷纷亮相2018世界人工智能大会,并登台演讲.关于人工智能的现状与未来,他们提出了各自的观点,也引 ...

最新文章

  1. 只需要30秒就可以做的30件事情:你会选择哪一件来改变世界?
  2. 牛客 CCA的区间 dp + 补集转移
  3. 商业智能常见名词浅释(转载)
  4. linux二进制数据16进制数据转换,[轉]16进制字符文本/二进制文件迷你互转器
  5. 计算机二级msoffice高级应用考试,全国计算机二级MSOffice高级应用考试大纲
  6. linux添加一块硬盘分区,centos6中添加一块新的硬盘并分区的方法介绍
  7. css如何使图片在右下角,这个右下角折角用css怎么画出来?
  8. Internet 选项在哪
  9. c语言水仙花数作业,c语言水仙花数(c语言水仙花数的编程)
  10. erlang中的ets和dets
  11. 直接在html打开ppt,PPT内如何直接看网页
  12. Redis数据库的使用
  13. 2019苹果全球开发者大会:起售价5999美元,史上最强大Mac电脑发布
  14. SQL数据库被标为可疑/置疑/质疑的处理
  15. handler机制--Handler使用
  16. JavaScript之DOM对象
  17. 设计一个长方体类Cuboid(Java)
  18. 大量数据进行数组操作的Redim Preserve替代方法
  19. 测控技术与仪器应该学计算机哪些,测控技术与仪器专业学什么
  20. python知识:如何自定义装饰器

热门文章

  1. 水中倒影-第13届蓝桥杯Scratch省赛真题第4题
  2. 重走长征路---OI每周刷题记录---9月21日 2013 AC 17题
  3. qpython3苹果下载软件_QPython3下载
  4. 建一座安全的“天空城” ——揭秘腾讯WeTest如何与祖龙共同挖掘手游安全漏洞...
  5. 怎么用C#制作印章(转自:http://dev.csdn.net/author/Knight94/d670c121ca4d49a09160cef171867f2d.html)
  6. 乐逗游戏独家运营TCL通讯阿尔卡特游戏中心
  7. 视频教程-桫哥-GOlang-09反射-Go语言
  8. 使用sock5代理连接tcp
  9. 2023最新自适应导航源码简约导航开源版+UI透明化很好看的
  10. 机器学习中的数学——深度学习优化的挑战:局部极小值