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


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

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 的異步值。

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函數,編譯器創建了一個帶有下面四種狀態的狀態機:

每個狀態表示函數中一個不同的暫停點。"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分支。

我們可以看到第一個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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM