前言
Futures(Rust
版的promises
)是Rust
异步的核心,Rust
标准库通过Futures trait
定义了一个异步任务的规范,不过仅仅实现了Future
还不足以实现异步,你还需要管理它们,你需要检查哪些Future
已经完成哪些还在等待,你还需要一个executor
和reactor
(类似Node.js
中的event loop
),这些不是Rust
提供的,Rust
团队将这些留给了社区来做,让社区来决定如何建立更好的异步生态,这看起来有些疯狂,不过你要知道,其实早期JavaScript
是没有Promise
的,这都是社区的贡献
正文
异步库
- Tokio (repo) (crates.io) (docs.rs)
- Async-std (repo) (crates.io) (docs.rs)
- Smol (repo) (crates.io) (docs.rs)
除此之外还有许多,不过这些已经足够。每个库都有其自己的受众,探讨谁更优秀并非明智之举。如果说要考虑哪个能最方便的解决问题,那答案显而易见是Tokio
,有些库本身就依赖Tokio
,如果不使用Tokio
的executor
就很难使用它们,这看上去有点绑架。不过Tokio
实际上也确实不赖,它有文档,有大量的社区贡献,也有不少可供学习的示例代码
在
Node.js
之前也有不少服务器端的JavaScript
实现,其中一些甚至是单线程,需要开发者自己使用fork
来处理阻塞。Tokio
类似Node.js
,它提供了一些异步方法供你使用。Smol
则类似Deno
,承诺更快更好
起步
我们在Cargo.toml
中添加Tokio
依赖,需要指明full
标记
[dependencies]
tokio = { version = "1", features = ["full"] }
features
用来暴露 conditional compilation,具体的取值是随意的,有些库用它来开启或关闭平台个性化代码。在Tokio
中则是用来按需引入子crates
,Tokio
曾经拆分成多个小模块,后来社区对其做了改变,用features
来做区分
在运行futures
之前必须启动一个executor
,Tokio
提供了一个宏在幕后帮你搞定了一切,将其添加到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()
}
不同于JavaScript
,Rust
中的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
会让你开始接触到Send
和Sync
相关的报错,通常是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<()> {
| +++++++++++++++++++
Send
和Sync
是Rust
实现并发的核心,它们是自动的trait
,这意味着如果所有组成类型都是Send
或Sync
那么Rust
自动给这个类型添加Send
或Sync
。这些trait
指明了一个类型是否可以在多个线程间安全的传送,如果没有这些结构,就会出现多线程之间数据的问题
幸运的是,很多Rust
类型都是Sync
和Send
的,你需要留意的只是如何消除错误,可以在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);
})
}
关于Sync
和Send
还有更多内容,这些我们以后再谈
相关阅读
总结
Rust
的异步编程是美妙的,光是这点就足够写24篇文章了。Rust
的内存机制可以保证你可以安全的编写多线程、异步代码,这就是你要开始加大力度把JavaScript
抛在脑后的地方。你可以在Node.js
中使用线程和web worker
,但这是一种妥协。在Rust
世界,我们不需要这种妥协