async
/.await
在第一章,我們簡要介紹了 async/.await
,並用它簡單架構一個簡易伺服器。本章節將深入探討 async/.await
的細節,解釋其運作原理,並比較 async
程式碼和傳統 Rust 程式的區別。
async
/.await
是特殊的 Rust 語法,使其能轉移控制權到當前執行緒,而非阻塞之,並在等待操作完成的同時,允許其他程式碼繼續推進。
使用 async
有兩個主要途徑:async fn
與 async
區塊(block)。兩者皆返回一個實作 Future
trait 的值:
# #![allow(unused_variables)] #fn main() { // `foo()` returns a type that implements `Future<Output = u8>`. // `foo().await` will result in a value of type `u8`. async fn foo() -> u8 { 5 } fn bar() -> impl Future<Output = u8> { // This `async` block results in a type that implements // `Future<Output = u8>`. async { let x: u8 = foo().await; x + 5 } } #}
如我們在第一章所見, async
函式主體和其他 future 都具有惰性:在執行前不做任何事。執行一個 Future
最常見的手段是 .await
它。當對一個 Future
呼叫 .await
時,會嘗試執行它到完成。若該 Future
阻塞,將會轉移控制權到當前的執行緒。而執行器會在該 Future
能取得更多進展時恢復執行它,讓 .await
得以解決。
async
生命週期
和其他傳統函式不同, async fn
會取得引用(reference)或其他非 'static
引數(argument),並返回一個綁定這些引數生命週期的 Future
。
# #![allow(unused_variables)] #fn main() { // This function: async fn foo(x: &u8) -> u8 { *x } // Is equivalent to this function: fn foo_expanded<'a>(x: &'a u8) -> impl Future<Output = u8> + 'a { async move { *x } } #}
這代表了從 async fn
返回的 future 只能在其非 '-static
引數的有效生命週期內被 await
。
在常見的例子像在呼叫函式(如 foo(&x).await
)立刻 await
該 future,這並不構成問題。不過,如果想保存或將這個 future 傳送到其他任務或執行緒中,這可能會發生問題。
常見將一個引用作為引數的 async fn
轉換為回傳一個 'static
future 的方法是,將呼叫 async fn
需要的引數包裹在一個 async
區塊裡:
# #![allow(unused_variables)] #fn main() { fn bad() -> impl Future<Output = u8> { let x = 5; borrow_x(&x) // ERROR: `x` does not live long enough } fn good() -> impl Future<Output = u8> { async { let x = 5; borrow_x(&x).await } } #}
透過將引數移動到該 async
區塊,我們延長了引數的生命週期,使其匹配 foo
回傳的 Future
。
async move
async
區塊(block) 與閉包(closure)可以使用 move
關鍵字,行為更類似一般的閉包。一個 async move
block 會取得它引用到的變數之所有權,這些變數就可以活過(outlive)當前的作用範圍(scope),但也就得與放棄其他程式碼共享這些變數的好處。
# #![allow(unused_variables)] #fn main() { /// `async` block: /// /// Multiple different `async` blocks can access the same local variable /// so long as they're executed within the variable's scope async fn blocks() { let my_string = "foo".to_string(); let future_one = async { // ... println!("{}", my_string); }; let future_two = async { // ... println!("{}", my_string); }; // Run both futures to completion, printing "foo" twice: let ((), ()) = futures::join!(future_one, future_two); } /// `async move` block: /// /// Only one `async move` block can access the same captured variable, since /// captures are moved into the `Future` generated by the `async move` block. /// However, this allows the `Future` to outlive the original scope of the /// variable: fn move_block() -> impl Future<Output = ()> { let my_string = "foo".to_string(); async move { // ... println!("{}", my_string); } } #}
在多執行緒的執行器上 .await
請注意,當使用多執行緒的 Future
執行器時,因為 .await
可能導致環境切換至新執行緒,讓 Future
可能在不同執行緒間移動,所以任何用在 async
裡的變數都必須能在執行緒間傳輸。
這代表使用 Rc
、&RefCell
或其他沒有實作 Send
trait 的型別,包含沒實作 Sync
trait 引用型別,都不安全。
(警告:只要不在呼叫 .await
的作用域裡,這些型別還是可以使用)
同樣地,在 .await
之間持有傳統非 future 的鎖不是個好主意,它可能會造成執行緒池(threadpool)完全鎖上:一個任務拿走了鎖,並且 .await
將控制權轉移至執行器,讓其他任務嘗試取得鎖,然後就造成死鎖(deadlock)。我們可以使用 futures::lock
而不是 std::sync
中的 Mutex
從而避免這件事。