在這個教程中我們將詳細分析rust異步代碼async/.await的內部運行機制。我們將使用async-std庫而不是tokio,因爲這是第一個支持async/.await語法的rust庫。async/.await原理解析教程分爲兩部分,這是第一部分。
區塊鏈開發教程鏈接:以太坊 | 比特幣 | EOS | Tendermint | Hyperledger Fabric | Omni/USDT | Ripple
0、準備Rust練習環境
首先讓我們先創建一個Cargo項目:
~$ cargo new --bin sleepus-interruptus
如果你期望和教程使用的編譯器保持一致,可以添加一個內容爲1.39.0的rust-toolchain文件。
在繼續下面的內容之前,先運行cargo run
確保環境沒有問題。
1、一個交替顯示的Rust程序
我們要寫一個簡單的程序,它可以顯示10次Sleepus消息,每次間隔0.5秒;同時顯示5次Interruptus消息,每次間隔1秒。下面是相當簡單的rust實現代碼:
use std::thread::{sleep};
use std::time::Duration;
fn sleepus() {
for i in 1..=10 {
println!("Sleepus {}", i);
sleep(Duration::from_millis(500));
}
}
fn interruptus() {
for i in 1..=5 {
println!("Interruptus {}", i);
sleep(Duration::from_millis(1000));
}
}
fn main() {
sleepus();
interruptus();
}
不過,上面的代碼會同步執行兩個操作,它會先顯示完所有的Sleepus消息,然後再顯示Interruptus消息。而我們期望的是這兩種消息交織顯示,也就是說Interruptus消息可以打斷Sleepus消息的顯示。
有兩個辦法可以實現交織顯示的目標。顯而易見的一個是爲每個函數創建一個單獨的線程,然後等待線程執行完畢。
use std::thread::{sleep, spawn};
fn main() {
let sleepus = spawn(sleepus);
let interruptus = spawn(interruptus);
sleepus.join().unwrap();
interruptus.join().unwrap();
}
需要指出的是:
- 我們使用
spawn(sleepus)
而不是spawn(sleepus())
來創建線程。後者將 立即執行sleepus()
然後將其執行結果傳給spawn
,這不是我們期望的- 我在主函數種使用join()
來等待子線程結束,並使用unwrap()
來處理 可以發生的故障,因爲我懶。
另一種實現方法是創建一個輔助線程,然後在主線程種調用其中一個函數:
fn main() {
let sleepus = spawn(sleepus);
interruptus();
sleepus.join().unwrap();
}
這種方法效率更高,因爲只需要額外創建一個線程,並且也沒有什麼副作用,因此我推薦使用這個方法。
不過這兩種方法都不是異步解決方案!我們使用兩個由操作系統管理的線程來併發執行兩個同步任務!接下來讓我們嘗試如何在單一線程內讓兩個任務協作執行!
2、用Rust異步async/.await實現交替顯示程序
我們將從較高層次的抽象開始,然後逐步深入rust異步編程的細節。現在讓我們以async風格重寫前面的應用。
首先在Cargo.toml中添加以下依賴:
async-std = { version = "1.2.0", features = ["attributes"] }
現在我們可以將應用重寫爲:
use async_std::task::{sleep, spawn};
use std::time::Duration;
async fn sleepus() {
for i in 1..=10 {
println!("Sleepus {}", i);
sleep(Duration::from_millis(500)).await;
}
}
async fn interruptus() {
for i in 1..=5 {
println!("Interruptus {}", i);
sleep(Duration::from_millis(1000)).await;
}
}
#[async_std::main]
async fn main() {
let sleepus = spawn(sleepus());
interruptus().await;
sleepus.await;
}
主要的修改說明如下:
- 我們不再使用std::thread中的sleep和spawn函數,而是採用async_std::task。- 在sleepus和interruptus函數前都加async
- 在調用sleep之後,我們補充了
.await
。注意不是.await()
調用,而是一個新語法 - 在主函數上使用
#[async_std::main]
屬性 - 主函數前也有async關鍵字
- 我們現在使用
spawn(sleepus())
而不是spawn(sleepus)
,這表示直接調用sleepus 並將結果傳給spawn - 對interruptus()的調用增加.await
- 對sleepus不再使用join(),而是改用.await語法
看起來有很多修改,不過實際上,我們的代碼結構和之前的版本基本是一致的。現在程序運行和我們的期望一致:採用單一線程進行無阻塞調用。
接下來讓我們分析上述修改到底意味着什麼。
3、async關鍵字的作用
在函數定義前添加async主要做了以下3個事:
- 這將允許你在函數體內使用.await語法。我們接下來會深入探討這一點
- 它修改了函數的返回類型。async fn foo() -> Bar 實際上返回的是
impl std::future::Future<Output=Bar>
- 它自動將結果值封裝進一個新的Future對象。我們下面會詳細展示這一點
現在讓我們展開說明第2點。在Rust的標準庫中有一個名爲Future的trait,Future有一個關聯類型Output。這個trait的意思是:我承諾當我完成任務時,會給你一個類型爲Output的值。例如你可以想象一個異步HTTP客戶端可能會這樣實現:
impl HttpRequest {
fn perform(self) -> impl Future<Output=HttpResponse> { ... }
}
在發送HTTP請求時需要一些無阻塞的I/O,我們並不希望阻塞調用線程,但是需要最終得到響應結果。
async fn sleepus()
的結果類型隱含爲()
。因此我們的Future的Output也應該爲()
。這意味着我們需要修改函數爲:
fn sleepus() -> impl std::future::Future<Output=()>
不過如果只修改這裏,編譯就會出現如下錯誤:
error[E0728]: `await` is only allowed inside `async` functions and blocks
--> src/main.rs:7:9
|
4 | fn sleepus() -> impl std::future::Future<Output=()> {
| ------- this is not `async`
...
7 | sleep(Duration::from_millis(500)).await;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only allowed inside `async` functions and blocks
error[E0277]: the trait bound `(): std::future::Future` is not satisfied
--> src/main.rs:4:17
|
4 | fn sleepus() -> impl std::future::Future<Output=()> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::future::Future` is not implemented for `()`
|
= note: the return type of a function must have a statically known size
第一個錯誤信息很直接:你只能在async函數或代碼塊中使用.await語法。我們還沒有接觸到異步代碼塊,不過看起來就是這樣:
async {
// async noises intensify
}
第二個錯誤消息就是第一個的結果:async關鍵字要求函數返回類型是impl Future
。如果沒有這個關鍵字,我們的loop結果類型是()
,這顯然不滿足要求。
將整個函數體用一個異步代碼塊包裹起來就解決問題了:
fn sleepus() -> impl std::future::Future<Output=()> {
async {
for i in 1..=10 {
println!("Sleepus {}", i);
sleep(Duration::from_millis(500)).await;
}
}
}
4、.await語法的作用
可能我們並不需要所有這些async/.await。如果我們移除sleepus的.await會怎麼樣?令人喫驚的是,居然編譯通過了,雖然給出了一個警告:
warning: unused implementer of `std::future::Future` that must be used
--> src/main.rs:8:13
|
8 | sleep(Duration::from_millis(500));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_must_use)]` on by default
= note: futures do nothing unless you `.await` or poll them
我們在生成一個Future值但沒有使用它。如果查看程序的輸出,你可以理解編譯器的警告是什麼意思了:
Interruptus 1
Sleepus 1
Sleepus 2
Sleepus 3
Sleepus 4
Sleepus 5
Sleepus 6
Sleepus 7
Sleepus 8
Sleepus 9
Sleepus 10
Interruptus 2
Interruptus 3
Interruptus 4
Interruptus 5
我們所有的Sleepus消息輸出都沒有延遲。問題在於對sleep的調用實際上沒有讓當前線程休息,它只是生成一個實現了Future的值,然後當承諾最終實現時,我們知道的確發生了延遲。但是由於我們簡單地忽略了Future,因此實際上沒有利用延遲。
爲了理解.await語法到底做了什麼,我們接下來直接使用Future值來實現我們的函數。首先從不用async塊開始。
5、不使用async關鍵字的Rust異步代碼
如果我們丟掉async代碼塊,看起來就是這樣:
fn sleepus() -> impl std::future::Future<Output=()> {
for i in 1..=10 {
println!("Sleepus {}", i);
sleep(Duration::from_millis(500));
}
}
這樣編譯會出現以下錯誤:
error[E0277]: the trait bound `(): std::future::Future` is not satisfied
--> src/main.rs:4:17
|
4 | fn sleepus() -> impl std::future::Future<Output=()> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::future::Future` is not implemented for `()`
|
上面錯誤是由於for循環的結果類型爲()
,它沒有實現Future這個trait。修復這個問題的一種辦法是在for循環後面加一句話使其返回Future的實現類型。我們已經知道可以用這個:sleep:
fn sleepus() -> impl std::future::Future<Output=()> {
for i in 1..=10 {
println!("Sleepus {}", i);
sleep(Duration::from_millis(500));
}
sleep(Duration::from_millis(0))
}
現在我們依然會看到在for循環內存在未使用的Future值的警告信息,不過返回值那個錯誤已經解決掉了。這個sleep調用實際上什麼也沒做,我們可以將其替換爲一個真正的佔位Future:
fn sleepus() -> impl std::future::Future<Output=()> {
for i in 1..=10 {
println!("Sleepus {}", i);
sleep(Duration::from_millis(500));
}
async_std::future::ready(())
}
6、實現自己的Future
爲了打破沙鍋問到底,讓我們再深入一步,不適用async_std庫中的ready函數,而是定義自己的實現Future的結構。讓我們稱之爲DoNothing。
use std::future::Future;
struct DoNothing;
fn sleepus() -> impl Future<Output=()> {
for i in 1..=10 {
println!("Sleepus {}", i);
sleep(Duration::from_millis(500));
}
DoNothing
}
問題在於DoNothing還沒有提供Future實現。我們接下來將進行一些編譯器驅動的開發,讓rustc告訴我們如何修復這個程序。第一個錯誤信息是:
the trait bound `DoNothing: std::future::Future` is not satisfied
因此讓我們補上這個trait的實現:
impl Future for DoNothing {
}
繼續報錯:
error[E0046]: not all trait items implemented, missing: `Output`, `poll`
--> src/main.rs:7:1
|
7 | impl Future for DoNothing {
| ^^^^^^^^^^^^^^^^^^^^^^^^^ missing `Output`, `poll` in implementation
|
= note: `Output` from trait: `type Output;`
= note: `poll` from trait: `fn(std::pin::Pin<&mut Self>, &mut std::task::Context<'_>) -> std::task::Poll<<Self as std::future::Future>::Output>`
我們還不是真正瞭解Pin<&mut Self>
或者Context
,不過我們知道Output
。因爲我們之前返回()
,現在讓我們照做。
use std::pin::Pin;
use std::task::{Context, Poll};
impl Future for DoNothing {
type Output = ();
fn poll(self: Pin<&mut Self>, ctx: &mut Context) -> Poll<Self::Output> {
unimplemented!()
}
}
喔!編譯通過了!當然在運行時它會失敗,因爲我們的unimplemented!()
調用:
thread 'async-std/executor' panicked at 'not yet implemented', src/main.rs:13:9
現在讓我們嘗試實現poll。我們需要返回一個值其類型爲Poll<Self::Output>
或者 Poll<()>
。讓我們看一下Poll的定義:
pub enum Poll<T> {
Ready(T),
Pending,
}
利用一些基本的推理,我們可以理解Ready表示“我們的Future已經完成,這是輸出”,而Pending表示“還沒完事兒”。假設我們的DoNothing希望立即返回()
類型的輸出,可以這樣:
fn poll(self: Pin<&mut Self>, _ctx: &mut Context) -> Poll<Self::Output> {
Poll::Ready(())
}
恭喜!你剛剛實現了自己的第一個Future結構!
7、async與函數返回值
還記得我們說過async對函數做的第三件事嗎:自動將結果值封裝爲一個新的Future。我們接下來展示這一點。
首先簡化sleepus的定義:
fn sleepus() -> impl Future<Output=()> {
DoNothing
}
編譯和運行正常。現在切換回async風格:
async fn sleepus() {
DoNothing
}
這時候會報錯:
error[E0271]: type mismatch resolving `<impl std::future::Future as std::future::Future>::Output == ()`
--> src/main.rs:17:20
|
17 | async fn sleepus() {
| ^ expected struct `DoNothing`, found ()
|
= note: expected type `DoNothing`
found type `()`
可以看到,當你有了一個async函數或代碼塊,結果會自動封裝到一個Future實現對象裏。因此我們需要返回一個impl Future<Output=DoNothing>
。現在我們的類型需要是Output=()
。
處理很簡單,只需要在DoNothing後面簡單添加.await:
async fn sleepus() {
DoNothing.await
}
這讓我們對.await的作用增加了一點直覺:它從DoNothing中提取Output值。不過,我們依然並不真正瞭解它是如何實現的。現在讓我們實現一個更復雜的Future來
繼續探索。