async-await 在 Rust 中是如何工作的?

異步編程非常有用,但很難學習。異步編程可以創建快速響應的應用程序,具有大量文件 I/O 或網絡 I/O 或響應式 GUI 的應用程序都通過異步編程獲益巨大。異步編程可以在許多語言中實現,每種語言都有不同的風格和語法,在 Rust 中,這個特性稱爲 async-await。

從 1.39.0 版本開始,async-await 就已經是 Rust 不可或缺的一部分了,大多數應用程序都依賴於社區 crates。本文將讓你深入瞭解 Rust 中的異步編程。

底層

async-await 的中心是 future 特性,它聲明瞭方法 poll(我將在下面更詳細地介紹這一點)。如果一個值可以異步計算,那麼相關的類型應該實現 future trait。反覆調用 poll 方法,直到最終值可用爲止。

此時,你可以從同步應用程序中手動重複調用 poll 方法,以獲得最終值。但是,因爲我談論的是異步編程,所以可以將此任務移交給另一個組件:運行時。因此,在使用 async 語法之前,必須有一個運行時。在下面的例子中,使用來自 tokio 社區的運行時。

讓 tokio 運行時可用的一個方便方法是在 main 函數上使用 #[tokio::main] 宏:

1#[tokio::main]
2async fn main(){
3    println!("Start!");
4    sleep(Duration::from_secs(1)).await;
5    println!("End after 1 second");
6}

當運行時可用時,你現在可以 await future。await 意味着只要 future 完成,就會停止進一步的執行。await 方法使運行時調用 poll 方法,這將驅動 future 完成。

在上面的例子中,tokios sleep 函數返回一個 future,當指定的持續時間過去時結束。通過等待這個 future,相關的輪詢方法將被重複調用,直到 future 完成。此外,main() 函數也會返回一個 future 值,因爲在 fn 之前有 async 關鍵字。

所以如果你看到一個標有 async 的函數:

1async fn foo() -> usize { /**/ }

async 實際上是一個語法糖,背後實現如下:

1fn foo() -> impl Future<Output = usize> { async { /**/ } }

Pinning 和 boxing

要深入瞭解 Rust 中的 async-await,必須瞭解 pinning 和 boxing。

有時需要保證對象不會在內存中移動。當你有一個自引用類型時,這就會生效:

1struct MustBePinned {
2    a: int16,
3    b: &int16
4}

如果成員 b 是一個引用,指向了同一實例中的成員 a。然後,當實例被移動時,引用 b 將變得無效,因爲成員 a 的位置已經更改,但 b 仍然指向前一個位置。因此,MustBePinned 實例不應該在內存中移動。像 MustBePinned 這樣的類型不應該實現 Unpin trait,因爲如果這樣就可以安全地在內存中移動了。換句話說,MustBePinned 是! Unpin。

默認情況下,Future 也是! Unpin 的,它不應該在內存中移動。那麼如何處理這些類型呢?

Pin 類型封裝指針類型,確保指針後面的值不會移動。Pin 類型通過不提供封裝類型的可變引用來確保這一點。類型將在對象的生命週期內固定,如果你不小心 Pin 住了實現 Unpin 的類型,它將不會有任何效果。

事實上,如果希望從函數返回 future (!Unpin),則必須對其進行 boxing。使用 Box 將導致類型分配到堆上而不是棧上,從而確保它可以比當前函數存活得更久而不被移動。特別是,如果你想交出一個 future,你只能交出一個指向它的指針,因爲 future 的類型必須是 Pin<Box>。

使用 async-wait 時,您肯定會遇到 boxing 和 pinning 語法,你需要記住以下幾點:

Future Trait

1pub trait Future {
2    type Output;
3
4    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
5}

下面是一個如何實現 future trait 的簡單例子:

 1struct  MyCounterFuture {
 2        cnt : u32,
 3        cnt_final : u32
 4}
 5
 6impl MyCounterFuture {
 7        pub fn new(final_value : u32) -> Self {
 8                Self {
 9                        cnt : 0,
10                        cnt_final : final_value
11                }
12        }
13}
14
15impl Future for MyCounterFuture {
16        type Output = u32;
17
18        fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<u32>{
19                self.cnt += 1;
20                if self.cnt >= self.cnt_final {
21                        println!("Counting finished");
22                        return Poll::Ready(self.cnt_final);
23                }
24
25                cx.waker().wake_by_ref();
26                Poll::Pending
27        }
28}
29
30#[tokio::main]
31async fn main(){
32        let my_counter = MyCounterFuture::new(42);
33
34        let final_value = my_counter.await;
35        println!("Final value: {}", final_value);
36}

用一個存儲在 cnt_final 中的值對 future 進行初始化。每次調用 poll 方法時,內部值 cnt 加 1。 如果 cnt 小於 cnt_final,則 future 向運行時的喚醒器發出信號,表明 future 已準備好再次輪詢。Poll::Pending 的返回值表示 future 還沒有完成。在 cnt>= cnt_final 之後,poll 函數返回 poll::Ready,表示 future 已完成並提供最終值。

附加信息:

本文翻譯自:

https://opensource.com/article/22/10/asynchronous-programming-rust

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/6QH6mG3hHoc5Ta5ZHi8baw