一文读懂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://www.006ib.com/119/delay/{}/url/http://www.8443311.com/599", 
    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版本请点这里下载

参考原文