【譯】Async/Await(三)——Aysnc/Await模式

原文標題:Async/Await
原文連接:https://os.phil-opp.com/async-await/#multitasking
公衆號: Rust 碎碎念
翻譯 by: Prayinghtml

Async/Await 模式(The Async/Await Pattern)

async/await 背後的思想是讓程序員可以像寫普通的同步代碼那樣來編寫代碼,由編譯器負責將其轉爲異步代碼。它基於asyncawait兩個關鍵字來發揮做用。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_futurepoll函數。若是它還沒有就緒,咱們就退出循環而後返回Poll::Pending。由於這種狀況下self仍處於WaitingOnFooTxt狀態,下一次的poll調用將會進入到相同的 match 分支而後重試對foo_txt_future輪詢。

foo_txt_future就緒後,咱們把結果賦予content變量而且繼續執行example函數的代碼:若是content.len()小於保存在狀態結構體裏的min_lenbar.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 的一個基本設計決定:在第一次輪詢以前,它們什麼都不作。

參考資料

[1]

嘗試在 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

相關文章
相關標籤/搜索