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 語法,你需要記住以下幾點:
-
Rust 不知道一個類型是否可以安全地移動。
-
不應該移動的類型必須封裝在 Pin 中。
-
大多數類型都是 Unpinned 類型。它們實現了 Unpin trait,並且可以在內存中自由移動。
-
如果一個類型被包裝在 Pin 中,並且被包裝的類型是! Unpin,則不可能從中獲取可變引用。
-
async 關鍵字創建的 future 是! Unpin 的,因此必須 pinned。
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 已完成並提供最終值。
附加信息:
-
使用 Box::pin 創建一個新的 pinned 和 boxed 類型。
-
Future crate 提供類型 BoxFuture,允許你將 future 定義爲函數的返回類型。
-
async_trait 允許你在 traits 中定義 async 函數 (這是目前不允許的)。
-
pin-utils crate 提供了用於 pin 值的宏。
-
tokio 的 try_join ! 宏等待多個 future 返回一個 Result<T, E>。
本文翻譯自:
https://opensource.com/article/22/10/asynchronous-programming-rust
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/6QH6mG3hHoc5Ta5ZHi8baw