0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Rust中Pin/Unpin详解

jf_wN0SrCdH 来源:Rust语言中文社区 2023-07-20 11:00 次阅读
有些事情你总是学了又忘记(或者说你从来就没学过?)

对我来说,其中之一就是在Rust中Pin/Unpin

每次我读到有关固定的解释,我的大脑就像 ,几周后就像 。

所以,我写这篇文章是为了强迫我的大脑记住这些知识。我们看看效果如何!

Pin

Pin 是一种指针,可以看作是&mut T&T之间的折中。Pin<&mut T>的重点是说:
  1. 这个值可以被修改(就像&mut T一样),但是
  2. 这个值不能被移动(不像&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处将控制权传递回调用者。这意味着尽管vx看起来像普通的堆栈变量,但在这里可能发生了更复杂的事情。


编译器希望生成类似这样的内容:

	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`. }


那么在这种状态(初始状态)下可以安全地移动。

	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>, ...) -> ... { ... }


当我们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 包裹之后不再可访问。如果我们看不到它,就无法移动它。 事实上我们可以更准确地表达不能移动规则:指向的值在值被丢弃之前不能移动(无论何时丢弃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::newPin::{into_inner,get_mut}T 的值,这意味着我们可以轻松地在“常规”的可变值之间进行转换,并忽略直接处理固定值所带来的复杂性。

UnpinTrait 是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 fnasync {}语法创建的任何 Future 都被视为!Unpin,这意味着一旦我们将其放入Pin中,就无法再取出来。

摘要

  • Pin是对另一个指针的包装,有点像&mut T,但额外的规则是在值被丢弃之前,移动它所指向的值是不安全的。
  • 为了安全地处理自引用结构,我们必须在设置自引用字段后防止其移动(使用Pin)。
  • Pin 承诺该值在其生命周期内无法移动,所以我们无法在不放弃创建&mut T的能力并破坏Pin的不变性的情况下创建它。
  • 当在拥有所有权的 Future 进行awaitFuture 时,编译器可以处理固定,因为它知道一旦所有权转移,Future就不会移动。
  • 否则,我们需要处理固定(例如使用pin!Box::pin
  • Unpin是一个标记特征,表示一个类型即使在被包装在Pin之后仍然可以安全地移动,使一切变得更简单。
  • 大多数结构是Unpin,但async fnasync {}总是产生!Unpin结构。
审核编辑:汤梓红

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 指针
    +关注

    关注

    1

    文章

    473

    浏览量

    70364
  • 数据结构
    +关注

    关注

    3

    文章

    564

    浏览量

    39905
  • 编辑器
    +关注

    关注

    1

    文章

    788

    浏览量

    30236
  • PIN
    PIN
    +关注

    关注

    1

    文章

    298

    浏览量

    23676
  • Rust
    +关注

    关注

    1

    文章

    223

    浏览量

    6387

原文标题:摘要

文章出处:【微信号:Rust语言中文社区,微信公众号:Rust语言中文社区】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    详解Rust的泛型

    所有的编程语言都致力于将重复的任务简单化,并为此提供各种各样的工具。在 Rust 中,泛型(generics)就是这样一种工具,它是具体类型或其它属性的抽象替代。在编写代码时,我们可以直接描述泛型的行为,以及与其它泛型产生的联系,而无须知晓它在编译和运行代码时采用的具体类型。
    发表于 11-12 09:08 899次阅读

    Rust GUI实践之Rust-Qt模块

    Rust-Qt 是 Rust 语言的一个 Qt 绑定库,它允许 Rust 开发者使用 Qt 框架来创建跨平台的图形界面应用程序。Qt 是一个跨平台的应用程序框架,它提供了一系列的工具和库,可以帮助
    的头像 发表于 09-30 16:43 972次阅读

    Rust语言如何与 InfluxDB 集成

    Rust 是一种系统级编程语言,具有高性能和内存安全性。InfluxDB 是一个开源的时间序列数据库,用于存储、查询和可视化大规模数据集。Rust 语言可以与 InfluxDB 集成,提供高效
    的头像 发表于 09-30 16:45 709次阅读

    只会用Python?教你在树莓派上开始使用Rust

    如果您对编程感兴趣,那么您可能听说过Rust。该语言由Mozilla设计,受到开发人员的广泛喜爱,并继续在奉献者成长。Raspberry Pi是小型计算机的瑞士军刀,非常适合学习代码。我们将两者
    发表于 05-20 08:00

    怎样去使用Rust进行嵌入式编程呢

    使用Rust进行嵌入式编程Use Rust for embedded development篇首语:Rust的高性能、可靠性和生产力使其适合于嵌入式系统。在过去的几年里,Rust在程序
    发表于 12-22 07:20

    RUST在嵌入式开发的应用是什么

    Rust是一种编程语言,它使用户能够构建可靠、高效的软件,尤其是用于嵌入式开发的软件。它的特点是:高性能:Rust具有惊人的速度和高内存利用率。可靠性:在编译过程可以消除内存错误。生产效率:优秀
    发表于 12-24 08:34

    如何利用C语言去调用rust静态库呢

    提示在rust的静态库libfoo.a也有__aeabi_ul2d的实现,与libgcc.a冲突。这点暂时没理解得太清楚,不过release版本编译的库没有引入这个实现$ cargo build
    发表于 06-21 10:27

    Rust代码中加载静态库时,出现错误 ` rust-lld: error: undefined symbol: malloc `怎么解决?

    时,出现错误 ` [i]rust-lld: error: undefined symbol: malloc `。如何将这些定义包含在我的静态库
    发表于 06-09 08:44

    rust-analyzer Rust编译器前端实现

    ./oschina_soft/rust-analyzer.zip
    发表于 05-19 09:23 2次下载
    <b class='flag-5'>rust</b>-analyzer <b class='flag-5'>Rust</b>编译器前端实现

    Rust在虚幻引擎5中的使用

    前段时间,研究了一套 Rust 接入 Maya Plugin 的玩法,主要原理还是使用 C ABI 去交互。那我想着 UE 是使用 C++ 写的,肯定也可以使用 C ABI 去交互,如果可以的话在 UE 中就可以使用 Rust 代码去跑,甚至还可以使用
    的头像 发表于 12-21 11:05 4680次阅读

    一文详解PIN二极管基本原理及设计

    【半导光电】PIN二极管基本原理及设计详解
    发表于 01-02 07:25 767次阅读

    重点讲解Send与Sync相关的并发知识

    Send与Sync在Rust中属于marker trait,代码位于marker.rs,在标记模块中还有Copy、Unpin等trait。
    的头像 发表于 01-16 09:54 615次阅读

    Rust的内部工作原理

    Rust到汇编:了解 Rust 的内部工作原理 非常好的Rust系列文章,通过生成的汇编代码,让你了解很多Rust内部的工作机制。例如文章有 Rus
    的头像 发表于 06-14 10:34 470次阅读
    <b class='flag-5'>Rust</b>的内部工作原理

    基于Rust的Log日志库介绍

    Rust是一门系统级编程语言,因其安全性、高性能和并发性而备受欢迎。在Rust应用程序中,日志记录是一项非常重要的任务,因为它可以帮助开发人员了解应用程序的运行情况并解决问题。Rust的Log库提供
    的头像 发表于 09-19 14:49 2152次阅读

    从Rustup出发看Rust编译生态

    从Rustup出发看Rust编译生态 1. Rust和LLVM的关系是怎样的? 2. Rustup中targets是什么,为什么可以安装多个? 3. Rust在windows上为什么需要安装Visual studio?
    的头像 发表于 01-02 11:00 240次阅读