Pinning

想要輪詢 future 前,必須先用一個特殊的型別 Pin<T> 固定住 future。如果你閱讀了前一節 揭秘:執行 Future 與任務Future trait 的解釋,你會在 Future:poll 方法的定義上 self: Pin<&mut Self> 認出 Pin,但它到底代表什麼,為什麼需要它?

為什麼要 Pinning

Pinning 達成了確保一個物件永遠不移動。我們需要憶起 async/.await 的運作原理,才能理解為什麼需要釘住一個物件。試想以下的程式碼:


# #![allow(unused_variables)]
#fn main() {
let fut_one = ...;
let fut_two = ...;
async move {
    fut_one.await;
    fut_two.await;
}
#}

神秘面紗之下,其實建立了一個實作了 Future 的匿名型別,它的 poll 方法如下:


# #![allow(unused_variables)]
#fn main() {
// The `Future` type generated by our `async { ... }` block
struct AsyncFuture {
    fut_one: FutOne,
    fut_two: FutTwo,
    state: State,
}

// List of states our `async` block can be in
enum State {
    AwaitingFutOne,
    AwaitingFutTwo,
    Done,
}

impl Future for AsyncFuture {
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
        loop {
            match self.state {
                State::AwaitingFutOne => match self.fut_one.poll(..) {
                    Poll::Ready(()) => self.state = State::AwaitingFutTwo,
                    Poll::Pending => return Poll::Pending,
                }
                State::AwaitingFutTwo => match self.fut_two.poll(..) {
                    Poll::Ready(()) => self.state = State::Done,
                    Poll::Pending => return Poll::Pending,
                }
                State::Done => return Poll::Ready(()),
            }
        }
    }
}
#}

第一次呼叫 poll 時,會輪詢 fut_one。若 fut_one 不能完成,AsyncFuture::poll 會直接返回。接下來的 Future 的 poll 呼叫則從前一次結束的地方繼續執行。這個過程會持續到整個 future 有辦法成功完成。

不過,如果 async 區塊(block)使用到引用(reference)會如何?舉例來說:


# #![allow(unused_variables)]
#fn main() {
async {
    let mut x = [0; 128];
    let read_into_buf_fut = read_into_buf(&mut x);
    read_into_buf_fut.await;
    println!("{:?}", x);
}
#}

這會編譯成怎樣的結構體(struct)呢?


# #![allow(unused_variables)]
#fn main() {
struct ReadIntoBuf<'a> {
    buf: &'a mut [u8], // points to `x` below
}

struct AsyncFuture {
    x: [u8; 128],
    read_into_buf_fut: ReadIntoBuf<'what_lifetime?>,
}
#}

這裡,ReadIntoBuf future 持有一個指向其他結構體 x 的引用。但是,若 AsyncFuture 移動了(moved),x 的記憶體位址也會隨之移動,並讓儲存在 read_into_buf_fut.buf 的指標失效。

將 future 固定在一個特定的記憶體位址可以避免這種問題發生,讓在 async 區塊裡建立值的引用更安全。

如何使用 Pinning

Pin 型別封裝了指標型別,保證這些在指標背後的值不被移動。例如 Pin<& mut>Pin<&t>Pin<Box<T>> 全都保證 T 不被移動。

大多數的型別不會有移不移動的問題。這些型別實作了 Unpin trait。指向 Unpin 型別的指標可任意移入 Pin 或從 Pin 取出。舉例來說,u8Unpin 的,因此 Pin<&mut T> 會表現得像普通的 &mut T

有些與 future 互動的函式會要求 future 必須為 Unpin。若想要在要求 Unpin 型別的函式使用一個非 UnpinFutureStream,你要先透過 Box::pin(可建立 Pin<Box<T>>)或 pin_utils::pin_mut! (可建立 Pin<&mut T>)來固定住(pin)這個值。Pin<Box<Fut>>Pin<&mut Fut> 就都能作為 future 使用,且都實作了 Unpin

以下是範例:


# #![allow(unused_variables)]
#fn main() {
use pin_utils::pin_mut; // `pin_utils` is a handy crate available on crates.io

// A function which takes a `Future` that implements `Unpin`.
fn execute_unpin_future(x: impl Future<Output = ()> + Unpin) { ... }

let fut = async { ... };
execute_unpin_future(fut); // Error: `fut` does not implement `Unpin` trait

// Pinning with `Box`:
let fut = async { ... };
let fut = Box::pin(fut);
execute_unpin_future(fut); // OK

// Pinning with `pin_mut!`:
let fut = async { ... };
pin_mut!(fut);
execute_unpin_future(fut); // OK
#}