0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

Rust中Pin/Unpin詳解

jf_wN0SrCdH ? 來源:Rust語言中文社區(qū) ? 2023-07-20 11:00 ? 次閱讀
有些事情你總是學了又忘記(或者說你從來就沒學過?)

對我來說,其中之一就是在Rust中Pin/Unpin。

每次我讀到有關固定的解釋,我的大腦就像 ,幾周后就像 。

所以,我寫這篇文章是為了強迫我的大腦記住這些知識。我們看看效果如何!

Pin

Pin 是一種指針,可以看作是&mut T&T之間的折中。Pin<&mut T>的重點是說:
  1. 這個值可以被修改(就像&mut T一樣),但是
  2. 這個值不能被移動(不像&mut T
為什么?因為有些值必須不能移動,或者需要特別小心地去移動。

一個典型的例子就是自指數(shù)據(jù)結構。在使用async時,它們會自然地出現(xiàn),因為未來值往往會在引用自己的本地值。


這個看似溫和的 Future:

	async fn self_ref() { let mut v = [1, 2, 3]; let x = &mut v[0]; tokio::from_secs(1)).await; *x = 42; }

需要一個自我引用的結構,因為在底層,futures是狀態(tài)機(不像閉包)。

請注意,self_ref在第一個await處將控制權傳遞回調用者。這意味著盡管vx看起來像普通的堆棧變量,但在這里可能發(fā)生了更復雜的事情。


編譯器希望生成類似這樣的內容:

	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.awaitorpollthem#[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`. }


那么在這種狀態(tài)(初始狀態(tài))下可以安全地移動。

	impl SelfRefFuture { fn new() -> Self { Self { state: SelfRefFutureState::Unresumed, v: MaybeUninit::uninit(), x: std::null_mut(), // .. } } }


只有當我們開始在f上進行輪詢時,我們才會遇到自我引用的問題(x指針被設置),但如果 f 被包裹在Pin中,所有這些移動都變成了unsafe,這正是我們想要的。 由于許多futures 一旦執(zhí)行就不應該在內存中移動,只有將它們包裝在Pin中才能安全地使用,因此與異步相關的函數(shù)往往接受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的函數(shù)

	fn poll(self: Pin<&mut Self>, ...) -> ... { ... }


當我們awaitf時,我們放棄了對它的所有權。 編譯器能夠為我們處理固定引用,但如果我們只提供一個&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 包裹之后不再可訪問。如果我們看不到它,就無法移動它。 事實上我們可以更準確地表達不能移動規(guī)則:指向的值在值被丟棄之前不能移動(無論何時丟棄Pin)。


這就是pin!宏的作用:它確保原始的f對我們的代碼不再可見,從而強制執(zhí)行Pin的不變性 Tokio’spin!是這樣實現(xiàn)的:

	// 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!有點更酷,但使用的是相同的原理:用新創(chuàng)建的Pin來遮蔽原始值,使其無法再被訪問和移動。

一個

所以Pin是一個指針(對另一個指針的零大小的包裝器),它有點像&mut T但有更多的規(guī)則。 下一個問題將是“歸還借用的數(shù)據(jù)”。 我們無法回到以前的固定未來

	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 }


現(xiàn)在應該更清楚為什么了:被固定的f現(xiàn)在是一個指針,它指向的數(shù)據(jù)(異步閉包)在我們從函數(shù)返回后將不再存在。 因此,我們可以使用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也是一個自動特性(編譯器在可能的情況下會自動實現(xiàn)它),它標記了一種類型,其值在被固定后可以被移動(例如,它不會自我引用)。


主要的觀點是,如果T: Unpin,我們總是可以Pin::newPin::{into_inner,get_mut}T 的值,這意味著我們可以輕松地在“常規(guī)”的可變值之間進行轉換,并忽略直接處理固定值所帶來的復雜性。

UnpinTrait 是Pin的一個重要限制,也是Box::pin如此有用的原因之一:當T: !Unpin時,“無法移動或替換Pin>的內部”,因此Box::pin(或者更準確地說是Box::into_pin)可以安全地調用不安全的Pin::new_unchecked,而得到的Box總是Unpin的,因為移動它時并不會移動實際的值。


這里說的很繞,我們用例子例子解釋一下。

另一個微小的例子

我們可以親手創(chuàng)造一個美好的 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 {} }

現(xiàn)在,我們可以多次調用它而不需要固定: 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 fnasync {}語法創(chuàng)建的任何 Future 都被視為!Unpin,這意味著一旦我們將其放入Pin中,就無法再取出來。

摘要

  • Pin是對另一個指針的包裝,有點像&mut T,但額外的規(guī)則是在值被丟棄之前,移動它所指向的值是不安全的。
  • 為了安全地處理自引用結構,我們必須在設置自引用字段后防止其移動(使用Pin)。
  • Pin 承諾該值在其生命周期內無法移動,所以我們無法在不放棄創(chuàng)建&mut T的能力并破壞Pin的不變性的情況下創(chuàng)建它。
  • 當在擁有所有權的 Future 進行awaitFuture 時,編譯器可以處理固定,因為它知道一旦所有權轉移,Future就不會移動。
  • 否則,我們需要處理固定(例如使用pin!Box::pin
  • Unpin是一個標記特征,表示一個類型即使在被包裝在Pin之后仍然可以安全地移動,使一切變得更簡單。
  • 大多數(shù)結構是Unpin,但async fnasync {}總是產生!Unpin結構。
審核編輯:湯梓紅

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權轉載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 指針
    +關注

    關注

    1

    文章

    480

    瀏覽量

    70564
  • 數(shù)據(jù)結構

    關注

    3

    文章

    573

    瀏覽量

    40133
  • 編輯器
    +關注

    關注

    1

    文章

    806

    瀏覽量

    31176
  • PIN
    PIN
    +關注

    關注

    1

    文章

    304

    瀏覽量

    24311
  • Rust
    +關注

    關注

    1

    文章

    228

    瀏覽量

    6611

原文標題:摘要

文章出處:【微信號:Rust語言中文社區(qū),微信公眾號:Rust語言中文社區(qū)】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    詳解Rust的泛型

    所有的編程語言都致力于將重復的任務簡單化,并為此提供各種各樣的工具。在 Rust ,泛型(generics)就是這樣一種工具,它是具體類型或其它屬性的抽象替代。在編寫代碼時,我們可以直接描述泛型的行為,以及與其它泛型產生的聯(lián)系,而無須知曉它在編譯和運行代碼時采用的具體類
    發(fā)表于 11-12 09:08 ?1065次閱讀

    如何在Rust讀寫文件

    見的內存安全問題和數(shù)據(jù)競爭問題。 在Rust,讀寫文件是一項非常常見的任務。本教程將介紹如何在Rust讀寫文件,包括基礎用法和進階用法。 基礎用法 讀取文件內容 使用 std::f
    的頭像 發(fā)表于 09-20 10:57 ?2046次閱讀

    Rust的多線程編程概念和使用方法

    Rust是一種強類型、高性能的系統(tǒng)編程語言,其官方文檔強調了Rust的標準庫具有良好的并發(fā)編程支持。Thread是Rust的一種并發(fā)編程
    的頭像 發(fā)表于 09-20 11:15 ?968次閱讀

    怎樣去使用Rust進行嵌入式編程呢

    使用Rust進行嵌入式編程Use Rust for embedded development篇首語:Rust的高性能、可靠性和生產力使其適合于嵌入式系統(tǒng)。在過去的幾年里,Rust在程序
    發(fā)表于 12-22 07:20

    RUST在嵌入式開發(fā)的應用是什么

    Rust是一種編程語言,它使用戶能夠構建可靠、高效的軟件,尤其是用于嵌入式開發(fā)的軟件。它的特點是:高性能:Rust具有驚人的速度和高內存利用率。可靠性:在編譯過程可以消除內存錯誤。生產效率:優(yōu)秀
    發(fā)表于 12-24 08:34

    Rust代碼中加載靜態(tài)庫時,出現(xiàn)錯誤 ` rust-lld: error: undefined symbol: malloc `怎么解決?

    時,出現(xiàn)錯誤 ` [i]rust-lld: error: undefined symbol: malloc `。如何將這些定義包含在我的靜態(tài)庫?
    發(fā)表于 06-09 08:44

    Linux內核整合對 Rust 的支持

    Linux Plumbers Conference 2022 大會上舉行了一個 Rust 相關的小型會議,該會議討論的大方向大致為:正在進行的使 Rust 成為一種合適的系統(tǒng)編程語言的工作,以及在主線 Linux 內核整合對
    的頭像 發(fā)表于 09-19 11:06 ?1180次閱讀

    RustGAT和高階類型

    Rust在類型系統(tǒng)級別上與Haskell,Scala有許多相似之處。
    的頭像 發(fā)表于 11-07 10:21 ?1178次閱讀

    Rust在虛幻引擎5的使用

    前段時間,研究了一套 Rust 接入 Maya Plugin 的玩法,主要原理還是使用 C ABI 去交互。那我想著 UE 是使用 C++ 寫的,肯定也可以使用 C ABI 去交互,如果可以的話在 UE 中就可以使用 Rust 代碼去跑,甚至還可以使用
    的頭像 發(fā)表于 12-21 11:05 ?6136次閱讀

    一文詳解PIN二極管基本原理及設計

    【半導光電】PIN二極管基本原理及設計詳解
    發(fā)表于 01-02 07:25 ?1415次閱讀

    重點講解Send與Sync相關的并發(fā)知識

    Send與Sync在Rust屬于marker trait,代碼位于marker.rs,在標記模塊還有Copy、Unpin等trait。
    的頭像 發(fā)表于 01-16 09:54 ?943次閱讀

    Rust的錯誤處理方法

    Rust 沒有提供類似于 Java、C++ 的 Exception 機制,而是使用 Result 枚舉的方式來實現(xiàn)。
    的頭像 發(fā)表于 02-20 09:37 ?960次閱讀

    rust語言基礎學習: rust的錯誤處理

    錯誤是軟件不可避免的,所以 Rust 有一些處理出錯情況的特性。在許多情況下,Rust 要求你承認錯誤的可能性,并在你的代碼編譯前采取一些行動。
    的頭像 發(fā)表于 05-22 16:28 ?2125次閱讀

    Rust的內部工作原理

    Rust到匯編:了解 Rust 的內部工作原理 非常好的Rust系列文章,通過生成的匯編代碼,讓你了解很多Rust內部的工作機制。例如文章有 Rus
    的頭像 發(fā)表于 06-14 10:34 ?798次閱讀
    <b class='flag-5'>Rust</b>的內部工作原理

    從Rustup出發(fā)看Rust編譯生態(tài)

    從Rustup出發(fā)看Rust編譯生態(tài) 1. Rust和LLVM的關系是怎樣的? 2. Rustuptargets是什么,為什么可以安裝多個? 3. Rust在windows上為
    的頭像 發(fā)表于 01-02 11:00 ?535次閱讀