写给前端看的Rust教程(18)异步

2,575 阅读5分钟

原文:24 days from node.js to Rust

前言

FuturesRust版的promises)是Rust异步的核心,Rust标准库通过Futures trait定义了一个异步任务的规范,不过仅仅实现了Future还不足以实现异步,你还需要管理它们,你需要检查哪些Future已经完成哪些还在等待,你还需要一个executorreactor(类似Node.js中的event loop),这些不是Rust提供的,Rust团队将这些留给了社区来做,让社区来决定如何建立更好的异步生态,这看起来有些疯狂,不过你要知道,其实早期JavaScript是没有Promise的,这都是社区的贡献

正文

异步库

除此之外还有许多,不过这些已经足够。每个库都有其自己的受众,探讨谁更优秀并非明智之举。如果说要考虑哪个能最方便的解决问题,那答案显而易见是Tokio,有些库本身就依赖Tokio,如果不使用Tokioexecutor就很难使用它们,这看上去有点绑架。不过Tokio实际上也确实不赖,它有文档,有大量的社区贡献,也有不少可供学习的示例代码

Node.js之前也有不少服务器端的JavaScript实现,其中一些甚至是单线程,需要开发者自己使用fork来处理阻塞。Tokio类似Node.js,它提供了一些异步方法供你使用。Smol则类似Deno,承诺更快更好

起步

我们在Cargo.toml中添加Tokio依赖,需要指明full标记

[dependencies]
tokio = { version = "1", features = ["full"] }

features用来暴露 conditional compilation,具体的取值是随意的,有些库用它来开启或关闭平台个性化代码。在Tokio中则是用来按需引入子cratesTokio曾经拆分成多个小模块,后来社区对其做了改变,用features来做区分

在运行futures之前必须启动一个executorTokio提供了一个宏在幕后帮你搞定了一切,将其添加到main()函数前面,你就得到了一个完整的异步Rust程序

#[tokio::main]
async fn main()  { // Notice we write async main() now
}

async/.await

Rust拥有和JavaScript类似的async/await风格的语法,给你一个函数添加async标记就会让这个函数的返回值从T变为impl Future<Output = T>,例如:

fn regular_fn() -> String {
  "I'm a regular function".to_owned()
}

async fn async_fn() -> String { // actually impl Future<Output = String>
  "I'm an async function".to_owned()
}

不同于JavaScriptRust中的await必须和一个具体的future绑定,不能放在前面,也不能用于future之外的其它值

#[tokio::main]
async fn main() {
    let msg = async_fn().await;
}

不同于JavaScript,在调用await之前future不会执行

#[tokio::main]
async fn main() {
    println!("One");
    let future = prints_two();
    println!("Three");
    // 将下面这行代码注释去掉之后再看看效果
    // future.await;
}

async fn prints_two() {
    println!("Two")
}

结果:

One
Three

去掉注释后的结果:

One
Three
Two

async block

异步和闭包是每个开发人员必备工具,你必然会在返回一个异步闭包的时候遇到如下的报错:

error[E0658]: async closures are unstable
 --> src/send-sync.rs:6:15
  |
6 |     let fut = async || {};
  |               ^^^^^
  |
  = note: see issue #62290 <https://github.com/rust-lang/rust/issues/62290> for more information
  = help: to use an async block, remove the `||`: `async {`

异步闭包不是很稳定,编译器会提示你是用异步区块(async block),什么是异步区块呢?

事实上所有的区块都可以异步化,它们实现了Future,并且可以像其它数据一样被返回

#[tokio::main]
async fn main() {
    let msg = "Hello world".to_owned();

    let async_block = || async {
        println!("{}", msg);
    };
    async_block().await;
}

你可以通过返回一个异步区块来得到异步闭包的全部能力

#[tokio::main]
async fn main() {
    let msg = "Hello world".to_owned();

    let closure = || async {
        println!("{}", msg);
    };
    closure().await;
}

Send + Sync

混合使用线程和futures会让你开始接触到SendSync相关的报错,通常是future cannot be sent between threads safely。下面的代码就展示了这种错误,这段代码无法编译:

use std::fmt::Display;
use tokio::task::JoinHandle;

#[tokio::main]
async fn main() {
    let mark_twain = "Samuel Clemens".to_owned();

    async_print(mark_twain).await;
}

fn async_print<T: Display>(msg: T) -> JoinHandle<()> {
    tokio::task::spawn(async move {
        println!("{}", msg);
    })
}

结果:

error: future cannot be sent between threads safely
   --> src/send-sync.rs:12:5
    |
12  |     tokio::task::spawn(async move {
    |     ^^^^^^^^^^^^^^^^^^ future created by async block is not `Send`
    |
note: captured value is not `Send`
   --> src/send-sync.rs:13:24
    |
13  |         println!("{}", msg);
    |                        ^^^ has type `T` which is not `Send`
note: required by a bound in `tokio::spawn`
   --> /Users/jsoverson/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.15.0/src/task/spawn.rs:127:21
    |
127 |         T: Future + Send + 'static,
    |                     ^^^^ required by this bound in `tokio::spawn`
help: consider further restricting this bound
    |
11  | fn async_print<T: Display + std::marker::Send>(msg: T) -> JoinHandle<()> {
    |                           +++++++++++++++++++

SendSyncRust实现并发的核心,它们是自动的trait,这意味着如果所有组成类型都是SendSync那么Rust自动给这个类型添加SendSync。这些trait指明了一个类型是否可以在多个线程间安全的传送,如果没有这些结构,就会出现多线程之间数据的问题

幸运的是,很多Rust类型都是SyncSend的,你需要留意的只是如何消除错误,可以在trait上简单的增加+ Send+ Sync或是+ Sync + Send

fn async_print<T: Display + Send>(msg: T) -> JoinHandle<()> {
    tokio::task::spawn(async move {
        println!("{}", msg);
    })
}

不过这么写的话又会遇到新的问题:

error[E0310]: the parameter type `T` may not live long enough
   --> src/send-sync.rs:12:5
    |
11  | fn async_print<T: Display + Send>(msg: T) -> JoinHandle<()> {
    |                -- help: consider adding an explicit lifetime bound...: `T: 'static +`
12  |     tokio::task::spawn(async move {
    |     ^^^^^^^^^^^^^^^^^^ ...so that the type `impl Future` will meet its required lifetime bounds...
    |

我们在 教程16 中遇到过'static的问题,由于Rust编译器知道无法断定异步代码何时执行,所以会提示我们T的存在时间可能不足,我们需要让Rust编译器知道该类型可以一直存在

fn async_print<T: Display + Send + 'static>(msg: T) -> JoinHandle<()> {
    tokio::task::spawn(async move {
        println!("{}", msg);
    })
}

关于SyncSend还有更多内容,这些我们以后再谈

相关阅读

总结

Rust的异步编程是美妙的,光是这点就足够写24篇文章了。Rust的内存机制可以保证你可以安全的编写多线程、异步代码,这就是你要开始加大力度把JavaScript抛在脑后的地方。你可以在Node.js中使用线程和web worker,但这是一种妥协。在Rust世界,我们不需要这种妥协