原文標題:Async/Await
原文連接:https://os.phil-opp.com/async-await/#multitasking
公衆號: Rust 碎碎念
翻譯 by: Prayinghtml
async/await 背後的思想是讓程序員可以像寫普通的同步代碼那樣來編寫代碼,由編譯器負責將其轉爲異步代碼。它基於async
和await
兩個關鍵字來發揮做用。async
關鍵字能夠被用於一個函數簽名,負責把一個同步函數轉爲一個返回 future 的異步函數。程序員
async fn foo() -> u32 {
0
}
// the above is roughly translated by the compiler to:
fn foo() -> impl Future<Output = u32> {
future::ready(0)
}
這個關鍵字是沒法單獨發揮做用的,可是在async
函數內部,await
關鍵字能夠被用於取回(retrieve)一個 future 的異步值。web
async fn example(min_len: usize) -> String {
let content = async_read_file("foo.txt").await;
if content.len() < min_len {
content + &async_read_file("bar.txt").await
} else {
content
}
}
(嘗試在 playground 上運行這段代碼[1])閉包
這個函數是對example
函數的一個直接轉換,example
函數使用了上面提到的組合子函數(譯註:在譯文 Async/Await(二)中)。經過使用.await
操做,咱們可以在不須要任何閉包或者Either
的狀況下檢索一個 future 的值。所以,咱們能夠像寫普通的同步代碼同樣來寫咱們的代碼,不一樣之處在於咱們寫的仍然是異步代碼。異步
編譯器在背後把async
函數體轉爲一個狀態機(state machine)[2],每個.await
調用表示一個不一樣的狀態。對於上面的example
函數,編譯器建立了一個帶有下面四種狀態的狀態機:async
每一個狀態表示函數中一個不一樣的暫停點。"Start"和"End"狀態表示開始執行的函數和執行結束的函數。"Waiting on foo.txt"狀態表示函數當前正在等待第一個async_read_file
的結果。相似地,"Waiting on bar.txt"表示函數正在等待第二個async_read_file
結果。編輯器
這個狀態機經過讓每個poll
調用成爲一次狀態轉換來實現Future
trait。函數
上面這張圖用箭頭表示狀態切換,用菱形表示分支路徑。例如,若是foo.txt
沒有準備好,就會選擇標記"no"的路徑而後進入」Waiting on foo.txt「狀態。不然,就會選擇"yes"路徑。中間較小的沒有標題的紅色菱形表示example
函數的if content.len() < 100
分支。oop
咱們能夠看到第一個poll
調用啓動了這個函數並使函數一直運行直到它到達一個還沒有就緒的 future。若是這條路徑上的全部 future 都已就緒,該函數就能夠一直運行到"End"狀態,這裏它把本身的結果包裝在Poll::Ready
中而後返回。不然,狀態機進入到一個等待狀態並返回"Poll::Pending"。在下一個poll
調用時,狀態機從上次等待狀態開始而後重試上次操做。佈局
爲了可以從上次等待狀態繼續下去,狀態機必須在內部記錄當前狀態。此外,它還必需要保存下次poll
調用時繼續執行須要的全部變量。這也正是編譯器大展身手的地方:由於編譯器知道哪一個變量在什麼時候被使用,因此它能夠自動生成結構體,這些結構體準確地包含了所須要的變量。
例如,編譯器能夠針對上面的example
函數生成相似下面的結構體:
// 再次放上`example` 函數 ,你就不用去上面找它了
async fn example(min_len: usize) -> String {
let content = async_read_file("foo.txt").await;
if content.len() < min_len {
content + &async_read_file("bar.txt").await
} else {
content
}
}
// 編譯器生成的狀態結構體:
struct StartState {
min_len: usize,
}
struct WaitingOnFooTxtState {
min_len: usize,
foo_txt_future: impl Future<Output = String>,
}
struct WaitingOnBarTxtState {
content: String,
bar_txt_future: impl Future<Output = String>,
}
struct EndState {}
在"Start"和"Waiting on foo.txt"這兩個狀態(分別對應 StartState 和 WaitingOnFooTxtState 結構體)裏,參數min_len
須要被存儲起來,由於在後面和content.len()
進行比較時會須要用到它。"Waiting on foo.txt"狀態還須要額外存儲一個foo_txt_future
,它表示由async_read_file
調用返回的 future。這個 future 在當狀態機繼續的時候會被再次輪詢(poll),因此它也須要被保存起來。
"Waiting on bar.txt"狀態(譯註:對應WaitingOnBarTxtState
結構體)包含了content
變量,由於它會在bar.txt
就緒後被用於字符串拼接。該狀態還存儲了一個bar_txt_future
用以表示對bar.txt
正在進行的加載。WaitingOnBarTxtState
結構體不包含min_len
變量由於它在和 content.len()
比較後就再也不被須要了。在"End"狀態下,沒有存儲任何變量,由於函數在這裏已經運行完成。
注意,這裏只是編譯器針對代碼可能生成的一個示例。結構體的命名以及字段的佈局都是實現細節而且可能有所不一樣。
雖然具體的編譯器生成代碼是一個實現細節,可是它有助於咱們理解example
函數生成的狀態機看起來是怎麼樣的?咱們已經定義了表示不一樣狀態的結構體而且包含須要的字段。爲了可以在此基礎上建立一個狀態機,咱們能夠把它組合進enum
:
enum ExampleStateMachine {
Start(StartState),
WaitingOnFooTxt(WaitingOnFooTxtState),
WaitingOnBarTxt(WaitingOnBarTxtState),
End(EndState),
}
咱們爲每一個狀態定義一個單獨的枚舉變量,而且把對應的狀態結構體添加到每一個變量中做爲一個字段。爲了實現狀態轉換,編譯器基於example
函數生成了一個Future
trait 的實現:
impl Future for ExampleStateMachine {
type Output = String; // return type of `example`
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
loop {
match self { // TODO: handle pinning
ExampleStateMachine::Start(state) => {…}
ExampleStateMachine::WaitingOnFooTxt(state) => {…}
ExampleStateMachine::WaitingOnBarTxt(state) => {…}
ExampleStateMachine::End(state) => {…}
}
}
}
}
future 的Output
類型是String
,由於它是example
函數的返回類型。爲了實現poll
函數,咱們在loop
內部對當前的狀態使用一個 match 語句。其思想在於只要有可能就切換到下一個狀態,當沒法繼續的時候就使用一個顯式的return Poll::Pending
。
簡單起見,咱們只能展現簡化的代碼且不對pinning[3]、全部權、生命週期等進行處理。因此,這段代碼以及接下來的代碼就當成是僞代碼,不要直接使用。固然,實際上編譯器生成的代碼已經正確地處理好了一切,儘管多是以另外一種方式。
爲了讓代碼片斷儘量地小,咱們爲每一個 match 分支單獨展現代碼。讓咱們先從Start
狀態開始:
ExampleStateMachine::Start(state) => {
// from body of `example`
let foo_txt_future = async_read_file("foo.txt");
// `.await` operation
let state = WaitingOnFooTxtState {
min_len: state.min_len,
foo_txt_future,
};
*self = ExampleStateMachine::WaitingOnFooTxt(state);
}
狀態機在函數開始時就處於Start
狀態,在這種狀況下,咱們從example
函數體執行全部的代碼,直至遇到第一個.await
。爲了處理.await
操做,咱們把self
狀態機的狀態更改成WaitingOnFooTxt
,該狀態包括了對WaitingOnFooTxtState
的構造。
由於match self {...}
狀態是在一個循環裏執行的,這個執行接下來跳轉到WaitingOnFooTxt
分支:
ExampleStateMachine::WaitingOnFooTxt(state) => {
match state.foo_txt_future.poll(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(content) => {
// from body of `example`
if content.len() < state.min_len {
let bar_txt_future = async_read_file("bar.txt");
// `.await` operation
let state = WaitingOnBarTxtState {
content,
bar_txt_future,
};
*self = ExampleStateMachine::WaitingOnBarTxt(state);
} else {
*self = ExampleStateMachine::End(EndState));
return Poll::Ready(content);
}
}
}
}
在這個 match 分支,咱們首先調用foo_txt_future
的poll
函數。若是它還沒有就緒,咱們就退出循環而後返回Poll::Pending
。由於這種狀況下self
仍處於WaitingOnFooTxt
狀態,下一次的poll
調用將會進入到相同的 match 分支而後重試對foo_txt_future
輪詢。
當foo_txt_future
就緒後,咱們把結果賦予content
變量而且繼續執行example
函數的代碼:若是content.len()
小於保存在狀態結構體裏的min_len
,bar.txt
文件會被異步地讀取。咱們再次把.await
操做轉換爲一個狀態改變,此次改變爲WaitingOnBarTxt
狀態。由於咱們在一個循環裏面正在執行match
,執行流程直接跳轉到新的狀態對應的 match 分支,這個新分支對bar_txt_future
進行了輪詢。
一旦咱們進入到else
分支,後面就再也不會進行.await
操做。咱們到達了函數結尾並返回包裝在Poll::Ready
中的content
。咱們還把當前的狀態改成了End
狀態。
WaitingOnBarTxt
狀態的代碼看起來像下面這樣:
ExampleStateMachine::WaitingOnBarTxt(state) => {
match state.bar_txt_future.poll(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(bar_txt) => {
*self = ExampleStateMachine::End(EndState));
// from body of `example`
return Poll::Ready(state.content + &bar_txt);
}
}
}
與WaitingOnFooTxt
狀態相似,咱們從輪詢bar_txt_future
開始。若是它仍然是 pending,咱們退出循環而後返回Poll::Pending
。不然,咱們能夠執行example
函數最後的操做:未來自 future 的結果與content
相鏈接。咱們把狀態機更新到End
狀態,而後將結果包裝在Poll::Ready
中進行返回。
最後,End
狀態的代碼看起來像下面這樣:
ExampleStateMachine::End(_) => {
panic!("poll called after Poll::Ready was returned");
}
在返回Poll::Ready
以後,future 不該該被再次輪詢。所以,當咱們已經處於End
狀態時,若是poll
被調用咱們將會 panic。
咱們如今知道編譯器生成的狀態機以及它對Future
trait 的實現是什麼樣子的了。實際上,編譯器是以一種不一樣的方式來生成代碼。(若是你感興趣的話,當前的實現是基於生成器(generator)[4]的,可是這只是一個實現細節)。
最後一部分是生成的示例函數自己的代碼。記住,函數簽名是這樣定義的:
async fn example(min_len: usize) -> String
由於完整的函數體實現是經過狀態機來實現的,這個函數惟一須要作的事情是初始化狀態機並將其返回。生成的代碼看起來像下面這樣:
fn example(min_len: usize) -> ExampleStateMachine {
ExampleStateMachine::Start(StartState {
min_len,
})
}
這個函數再也不有async
修飾符,由於它如今顯式地返回一個ExampleStateMachine
類型,這個類型實現了Future
trait。正如所指望的,狀態機在Start
狀態被構造,並使用min_len
參數初始化與之對應的狀態結構體。
記住,這個函數沒有開始狀態機的執行。這是 Rust 中 future 的一個基本設計決定:在第一次輪詢以前,它們什麼都不作。
嘗試在 playground 上運行這段代碼: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=d93c28509a1c67661f31ff820281d434
[2]狀態機(state machine): https://en.wikipedia.org/wiki/Finite-state_machine
[3]pinning: https://doc.rust-lang.org/stable/core/pin/index.html
[4]生成器(generator): https://doc.rust-lang.org/nightly/unstable-book/language-features/generators.html