有些事情你總是學了又忘記(或者說你從來就沒學過?)
對我來說,其中之一就是在Rust中
每次我讀到有關固定的解釋,我的大腦就像 ,幾周后就像 。
所以,我寫這篇文章是為了強迫我的大腦記住這些知識。我們看看效果如何!
一個典型的例子就是自指數據結構。在使用
這個看似溫和的 Future:
需要一個自我引用的結構,因為在底層,
請注意,
編譯器希望生成類似這樣的內容:
但是!如果你想的話,你可以移動
怎么回事?就像一位聰明的編譯器曾經說過的:
那么在這種狀態(初始狀態)下可以安全地移動。
只有當我們開始在
但是如果我們想要多次調用 timeout (例如,因為我們想要重試),我們將不得不使用
為什么? 因為在幾個層級下,
當我們
這個時候我們需要給 future 套上一個
這里還需要再做一點額外的工作,我們需要確保
這就是
標準庫的版本
現在應該更清楚為什么了:被固定的
但是我們剛剛不是說
主要的觀點是,如果
這里說的很繞,我們用例子例子解釋一下。
現在,我們可以多次調用它而不需要固定: timeout
使用
對我來說,其中之一就是在Rust中
Pin/Unpin
。每次我讀到有關固定的解釋,我的大腦就像 ,幾周后就像 。
所以,我寫這篇文章是為了強迫我的大腦記住這些知識。我們看看效果如何!
Pin
Pin 是一種指針,可以看作是&mut T
和&T
之間的折中。Pin<&mut T>
的重點是說:
-
這個值可以被修改(就像
&mut T
一樣),但是 -
這個值不能被移動(不像
&mut T
)
一個典型的例子就是自指數據結構。在使用
async
時,它們會自然地出現,因為未來值往往會在引用自己的本地值。這個看似溫和的 Future:
async fn self_ref() { let mut v = [1, 2, 3]; let x = &mut v[0]; tokio::from_secs(1)).await; *x = 42; }
需要一個自我引用的結構,因為在底層,
futures
是狀態機(不像閉包)。請注意,
self_ref
在第一個await
處將控制權傳遞回調用者。這意味著盡管v
和x
看起來像普通的堆棧變量,但在這里可能發生了更復雜的事情。編譯器希望生成類似這樣的內容:
enum SelfRefFutureState { Unresumed, // Created and wasn't polled yet. Returned, Poisoned, // `panic!`ed. SuspensionPoint1, // First `await` point. } struct SelfRefFuture { state: SelfRefFutureState, v: [i32; 3], x: &'problem mut i32, // a "reference" to an element of `self.v`, // which is a big problem if we want to move `self`. // (and we didn't even consider borrowchecking!) }
但是!如果你想的話,你可以移動
SelfRefFuture
,這會導致x
指向無效的內存。
let f = self_ref(); let boxed_f = Box::new(f); // Evil? let mut f1 = self_ref(); let mut f2 = self_ref(); std::swap(&mut f1, &mut f2); // Blasphemy?
怎么回事?就像一位聰明的編譯器曾經說過的:
futures do nothing unless you這是因為調用.await
orpoll
them#[warn(unused_must_use)]
on by default – rustc
self_ref
實際上什么都不做, 我們實際上會得到類似于:
struct SelfRefFuture { state: SelfRefFutureState, v: MaybeUninit<[i32; 3]>, x: *mut i32, // a pointer into `self.v`, // still a problem if we want to move `self`, but only after it is set. // // .. other locals, like the future returned from `tokio::sleep`. }
那么在這種狀態(初始狀態)下可以安全地移動。
impl SelfRefFuture { fn new() -> Self { Self { state: SelfRefFutureState::Unresumed, v: MaybeUninit::uninit(), x: std::null_mut(), // .. } } }
只有當我們開始在
f
上進行輪詢時,我們才會遇到自我引用的問題(x
指針被設置),但如果 f 被包裹在Pin
中,所有這些移動都變成了unsafe
,這正是我們想要的。 由于許多futures 一旦執行就不應該在內存中移動,只有將它們包裝在Pin
中才能安全地使用,因此與異步相關的函數往往接受Pin<&mut T>
(假設它們不需要移動該值)。
一個微小的例子
這里不需要固定:use tokio::timeout; async fn with_timeout_once() { let f = async { 1u32 }; let _ = timeout(Duration::from_secs(1), f).await; }
但是如果我們想要多次調用 timeout (例如,因為我們想要重試),我們將不得不使用
&mut f
(否則會得到use of moved value
),這將導致編譯器報錯
use tokio::timeout; async fn with_timeout_twice() { let f = async { 1u32 }; // error[E0277]: .. cannot be unpinned, consider using `Box::pin`. // required for `&mut impl Future ` to implement `Future`let _ = timeout(Duration::from_secs(1), &mut f).await; // An additional retry. let _ = timeout(Duration::from_secs(1), &mut f).await; }
為什么? 因為在幾個層級下,
timeout
調用了被定義為Future::poll
的函數
fn poll(self: Pin<&mut Self>, ...) -> ... { ... }
當我們
await
f
時,我們放棄了對它的所有權。 編譯器能夠為我們處理固定引用,但如果我們只提供一個&mut f
,它就無法做到這一點,因為我們很容易破壞 Pin 的不變性:
use tokio::timeout; async fn with_timeout_twice_with_move() { let f = async { 1u32 }; // error[E0277]: .. cannot be unpinned, consider using `Box::pin`. let _ = timeout(Duration::from_secs(1), &mut f).await; // .. because otherwise, we could move `f` to a new memory location, after it was polled! let f = *Box::new(f); let _ = timeout(Duration::from_secs(1), &mut f).await; }
這個時候我們需要給 future 套上一個
pin!
use tokio::pin; use tokio::timeout; async fn with_timeout_twice() { let f = async { 1u32 }; pin!(f); // f is now a `Pin<&mut impl Future >`.let _ = timeout(Duration::from_secs(1), &mut f).await; let _ = timeout(Duration::from_secs(1), &mut f).await; }
這里還需要再做一點額外的工作,我們需要確保
f
在被 pin 包裹之后不再可訪問。如果我們看不到它,就無法移動它。 事實上我們可以更準確地表達不能移動規則:指向的值在值被丟棄之前不能移動(無論何時丟棄Pin
)。這就是
pin!
宏的作用:它確保原始的f
對我們的代碼不再可見,從而強制執行Pin
的不變性 Tokio’spin!
是這樣實現的:
// Move the value to ensure that it is owned let mut f = f; // Shadow the original binding so that it can't be directly accessed // ever again. #[allow(unused_mut)] let mut f = unsafe { Pin::new_unchecked(&mut f) };
標準庫的版本
pin!
有點更酷,但使用的是相同的原理:用新創建的Pin
來遮蔽原始值,使其無法再被訪問和移動。
一個
所以Pin
是一個指針(對另一個指針的零大小的包裝器),它有點像&mut T
但有更多的規則。 下一個問題將是“歸還借用的數據”。 我們無法回到以前的固定未來
use std::Future; async fn with_timeout_and_return() -> impl Future { let f = async { 1u32 }; pin!(f); // f is now a `Pin<&mut impl Future>`.let s = async move { let _ = timeout(Duration::from_secs(1), &mut f).await; }; // error[E0515]: cannot return value referencing local variable `f` s }
現在應該更清楚為什么了:被固定的
f
現在是一個指針,它指向的數據(異步閉包)在我們從函數返回后將不再存在。 因此,我們可以使用Box::pin
-pin!(f); +let mut f = Box::pin(f);
但是我們剛剛不是說
Pin<&mut T>
是&mut T
和&T
之間的(一個包裝器)指針嗎? 嗯,一個mut Box
也像一個&mut T
,但有所有權。 所以一個Pin>
是一個指向可變Box
和不可變Box
之間的指針,值可以被修改但不能被移動。
Unpin
Unpin
是一種 Trait。它不是Pin
的"相反",因為Pin
是指針的一種類型,而特征不能成為指針的相反。Unpin
也是一個自動特性(編譯器在可能的情況下會自動實現它),它標記了一種類型,其值在被固定后可以被移動(例如,它不會自我引用)。主要的觀點是,如果
T: Unpin
,我們總是可以Pin::new
和Pin::{into_inner,get_mut}
T 的值,這意味著我們可以輕松地在“常規”的可變值之間進行轉換,并忽略直接處理固定值所帶來的復雜性。Unpin
Trait 是Pin
的一個重要限制,也是Box::pin
如此有用的原因之一:當T: !Unpin
時,“無法移動或替換Pin>
的內部”,因此Box::pin
(或者更準確地說是Box::into_pin
)可以安全地調用不安全的Pin::new_unchecked
,而得到的Box
總是Unpin
的,因為移動它時并不會移動實際的值。這里說的很繞,我們用例子例子解釋一下。
另一個微小的例子
我們可以親手創造一個美好的 Future:fn not_self_ref() -> impl Future u32> + Unpin { struct Trivial {} impl Future for Trivial { type Output = u32; fn poll(self: Pin<&mut Self>, _cx: &mut std::Context<'_>) -> std::Poll { std::Ready(1) } } Trivial {} }
現在,我們可以多次調用它而不需要固定: timeout
async fn not_self_ref_with_timeout() { let mut f = not_self_ref(); let _ = timeout(Duration::from_secs(1), &mut f).await; let _ = timeout(Duration::from_secs(1), &mut f).await; }
使用
async fn
或async {}
語法創建的任何 Future 都被視為!Unpin
,這意味著一旦我們將其放入Pin
中,就無法再取出來。
摘要
-
Pin
是對另一個指針的包裝,有點像&mut T
,但額外的規則是在值被丟棄之前,移動它所指向的值是不安全的。 -
為了安全地處理自引用結構,我們必須在設置自引用字段后防止其移動(使用
Pin
)。 -
Pin 承諾該值在其生命周期內無法移動,所以我們無法在不放棄創建
&mut T
的能力并破壞Pin
的不變性的情況下創建它。 -
當在擁有所有權的 Future 進行
await
Future 時,編譯器可以處理固定,因為它知道一旦所有權轉移,Future
就不會移動。 -
否則,我們需要處理固定(例如使用
pin!
或Box::pin
) -
Unpin
是一個標記特征,表示一個類型即使在被包裝在Pin
之后仍然可以安全地移動,使一切變得更簡單。 -
大多數結構是
Unpin
,但async fn
和async {}
總是產生!Unpin
結構。
審核編輯:湯梓紅
聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。
舉報投訴
-
指針
+關注
關注
1文章
480瀏覽量
70551 -
數據結構
+關注
關注
3文章
573瀏覽量
40123 -
編輯器
+關注
關注
1文章
805瀏覽量
31163 -
PIN
+關注
關注
1文章
304瀏覽量
24284 -
Rust
+關注
關注
1文章
228瀏覽量
6601
原文標題:摘要
文章出處:【微信號:Rust語言中文社區,微信公眾號:Rust語言中文社區】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
詳解Rust的泛型
所有的編程語言都致力于將重復的任務簡單化,并為此提供各種各樣的工具。在 Rust 中,泛型(generics)就是這樣一種工具,它是具體類型或其它屬性的抽象替代。在編寫代碼時,我們可以直接描述泛型的行為,以及與其它泛型產生的聯系,而無須知曉它在編譯和運行代碼時采用的具體類
發表于 11-12 09:08
?1058次閱讀
如何在Rust中讀寫文件
見的內存安全問題和數據競爭問題。 在Rust中,讀寫文件是一項非常常見的任務。本教程將介紹如何在Rust中讀寫文件,包括基礎用法和進階用法。 基礎用法 讀取文件內容 使用 std::f
Rust的多線程編程概念和使用方法
Rust是一種強類型、高性能的系統編程語言,其官方文檔中強調了Rust的標準庫具有良好的并發編程支持。Thread是Rust中的一種并發編程
怎樣去使用Rust進行嵌入式編程呢
使用Rust進行嵌入式編程Use Rust for embedded development篇首語:Rust的高性能、可靠性和生產力使其適合于嵌入式系統。在過去的幾年里,Rust在程序
發表于 12-22 07:20
RUST在嵌入式開發中的應用是什么
Rust是一種編程語言,它使用戶能夠構建可靠、高效的軟件,尤其是用于嵌入式開發的軟件。它的特點是:高性能:Rust具有驚人的速度和高內存利用率。可靠性:在編譯過程中可以消除內存錯誤。生產效率:優秀
發表于 12-24 08:34
在Rust代碼中加載靜態庫時,出現錯誤 ` rust-lld: error: undefined symbol: malloc `怎么解決?
時,出現錯誤 ` [i]rust-lld: error: undefined symbol: malloc `。如何將這些定義包含在我的靜態庫中?
發表于 06-09 08:44
Linux內核中整合對 Rust 的支持
Linux Plumbers Conference 2022 大會上舉行了一個 Rust 相關的小型會議,該會議討論的大方向大致為:正在進行的使 Rust 成為一種合適的系統編程語言的工作,以及在主線 Linux 內核中整合對
Rust在虛幻引擎5中的使用
前段時間,研究了一套 Rust 接入 Maya Plugin 的玩法,主要原理還是使用 C ABI 去交互。那我想著 UE 是使用 C++ 寫的,肯定也可以使用 C ABI 去交互,如果可以的話在 UE 中就可以使用 Rust 代碼去跑,甚至還可以使用
重點講解Send與Sync相關的并發知識
Send與Sync在Rust中屬于marker trait,代碼位于marker.rs,在標記模塊中還有Copy、Unpin等trait。
rust語言基礎學習: rust中的錯誤處理
錯誤是軟件中不可避免的,所以 Rust 有一些處理出錯情況的特性。在許多情況下,Rust 要求你承認錯誤的可能性,并在你的代碼編譯前采取一些行動。
從Rustup出發看Rust編譯生態
從Rustup出發看Rust編譯生態
1. Rust和LLVM的關系是怎樣的?
2. Rustup中targets是什么,為什么可以安裝多個?
3. Rust在windows上為
評論