涵蓋
Pin、Unpin、pin!macro、tokio::pin!,以及它們背後的設計邏輯。
一、問題的根源:自引用結構
要理解 Pin,得先知道它解決的是什麼問題。
在 Rust 中,「移動(move)」一個值,本質上是把它的記憶體內容複製到新位置,然後讓舊的失效。大多數情況下這完全沒問題,但有一種情況會爆炸——自引用結構(self-referential struct):
struct SelfRef {
data: String,
ptr: *const String, // 指向自己的 data!
}如果這個結構被移動了,data 跑到新地址,但 ptr 還指著舊地址,就成了懸空指標(dangling pointer),記憶體安全直接玩完。
這聽起來像是很少見的邊緣案例,但其實非常普遍——async/await 產生的 Future 幾乎都是自引用結構:
async fn example() {
let data = vec![1, 2, 3];
some_await().await;
println!("{:?}", data); // Future 需要跨 await 持有對 data 的引用
}編譯器為這個 async fn 生成的狀態機,會同時持有 data 和指向它的引用,這就是自引用。
二、Pin 的核心思想
Pin<P> 是一個包裝器,它對指標 P(如 &mut T 或 Box<T>)做出保證:
「被指向的值
T,在它被 drop 之前,不會再被移動。」
use std::pin::Pin;
// 普通的 Box,裡面的值可以被 move 出來
let b: Box<String> = Box::new("hello".into());
let s: String = *b; // ✅ 可以 move
// Pin 住的 Box,裡面的值無法被安全地 move 出來
let p: Pin<Box<String>> = Box::pin("hello".into());
// let s: String = *p; // ❌ 無法取得 &mut T 來 move
Pin 本身不是什麼魔法鎖,而是透過型別系統來防止你取得能夠 move 該值的 &mut T。
三、Unpin:大多數型別其實不在乎
Rust 有個 auto trait 叫 Unpin:
- 實作了
Unpin的型別,即使被 Pin 住也可以安全地移動,因為它們不是自引用結構。 - 幾乎所有普通型別(
i32、String、Vec、…)都自動實作Unpin。 async產生的 Future 不實作Unpin,因為它們可能是自引用的。
普通型別(String、Vec...)→ 自動 impl Unpin → Pin 對它形同虛設
async fn 的 Future → 不 impl Unpin → Pin 真正發揮作用
四、Pin 的兩個重要方法
// 如果 T: Unpin,可以安全拿到 &mut T
impl<P: DerefMut<Target: Unpin>> Pin<P> {
pub fn get_mut(self) -> &mut T
}
// 不管 T 是否 Unpin,都能繼續操作 Pin 住的值
impl<P: DerefMut> Pin<P> {
pub fn as_mut(&mut self) -> Pin<&mut T>
}五、手動實作 Future 時的 Pin
Pin 最直接的使用場景之一是手動實作 Future:
impl Future for MyFuture {
type Output = i32;
// self 必須是 Pin<&mut Self>
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<i32> {
todo!()
}
}六、pin! macro:把值 pin 在 Stack 上
Box::pin() 把值放在 heap 上,有分配成本。很多情況下我們希望把 Future pin 在 stack 上,這時就需要 pin! macro。
手動在 stack 上 pin 既麻煩又需要 unsafe:
let mut future = some_async_fn();
// 你必須保證之後不會 move future
let mut pinned = unsafe { Pin::new_unchecked(&mut future) };pin! macro 讓這件事變得安全且方便。
tokio::pin!
tokio::pin! 接受變數名稱,直接在當前 scope 做 shadowing:
// tokio::pin! 大致展開成這樣:
macro_rules! pin {
($x:ident) => {
let mut $x = $x; // (1) move 到新綁定
let mut $x = unsafe { Pin::new_unchecked(&mut $x) }; // (2) shadow 成 Pin<&mut T>
}
}使用方式:
use tokio::pin;
let future = some_async_fn();
pin!(future); // future 的型別現在是 Pin<&mut impl Future>
future.await;步驟 (1) 把值 move 到新綁定,步驟 (2) 把名字蓋掉,改成 Pin<&mut T>。此後你只看得到 Pin 版本,原本可以 move 的那個綁定已不可見,借用規則自然防止你移動它。
std::pin::pin!(Rust 1.68+)
標準庫的版本接受任意表達式,靠的是另一個機制——Temporary Lifetime Extension(暫存值生命週期延伸):
use std::pin::pin;
let fut = pin!(some_async_fn());展開後:
let fut = {
let mut _pinned = some_async_fn();
unsafe { Pin::new_unchecked(&mut _pinned) }
};Rust 編譯器發現回傳值裡含有對 block 內部暫存值的引用,於是自動把 _pinned 的生命週期延伸到與 fut 相同的 scope,確保不會出現懸空指標。這是編譯器特殊規則,不是 unsafe hack。
兩者對比
tokio::pin! |
std::pin::pin! |
|
|---|---|---|
| 接受 | 變數名 ident |
任意表達式 expr |
| 機制 | 變數 shadowing | Temporary lifetime extension |
| 語法 | pin!(fut); |
let fut = pin!(expr); |
七、「move 進 pin! 不就跟 Pin 的目的矛盾?」
這是個很常見的疑惑。答案是:move 發生在 pin 之前,Pin 的承諾是從 pin 的那一刻之後才開始的。
Pin 的合約不是「這個值永遠不能移動」,而是:
「一旦被 pin 住之後,就不能再移動。」
async fn 產生的 Future,在第一次被 poll 之前,內部根本還沒有任何自引用,只是一個普通的初始狀態機。自引用是在 poll 的過程中才逐漸形成的:
[建立 Future] → move 進 _pinned → [Pin 住] → [第一次 poll] → 自引用開始形成
✅ 安全 ✅ 此後不再 move
所以 pin! 在 pin 之前 move 值是完全合法的,並不矛盾。
八、最重要的使用場景:select!
這是你最常需要 pin! 的地方。tokio::select! 需要能夠跨多次 poll 同一個 Future,每次都必須是同一個 pinned Future,不能 move 它:
use tokio::{pin, time};
use std::time::Duration;
#[tokio::main]
async fn main() {
let long_operation = do_something_slow();
pin!(long_operation);
let mut interval = time::interval(Duration::from_secs(1));
loop {
tokio::select! {
result = &mut long_operation => {
println!("完成:{:?}", result);
break;
}
_ = interval.tick() => {
println!("還在等待中...");
}
}
}
}如果沒有 pin!,每次進入 loop 都會 move long_operation,之前的 poll 狀態就丟失了。
九、什麼時候用哪個?
| 情境 | 建議 |
|---|---|
| 現代專案,不依賴 Tokio | std::pin::pin!(Rust 1.68+) |
| Tokio 專案 | tokio::pin! 或 std::pin::pin! 皆可 |
| 需要跨執行緒傳遞 | Box::pin()(heap 分配,Box 本身可 move) |
| 效能敏感,避免 heap 分配 | pin!(stack) |
總結
- Pin 是 Rust 用型別系統做出的「這個值不能再被移動」的承諾,專為自引用結構(尤其是 async Future)而設計。
- Unpin 讓普通型別不受 Pin 影響,只有真正需要固定位置的型別(如 async Future)才會受到約束。
tokio::pin!靠 shadowing,std::pin::pin!靠 temporary lifetime extension,兩者都能安全地把值 pin 在 stack 上。- Pin 的承諾從「pin 的那一刻」開始,pin 之前的 move 完全合法。