有些事情你总是学了又忘记(或者说你从来就没学过?)
对我来说,其中之一就是在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
结构。
审核编辑:汤梓红
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表德赢Vwin官网
网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。
举报投诉
-
指针
+关注
关注
1文章
480浏览量
70551 -
数据结构
+关注
关注
3文章
573浏览量
40121 -
编辑器
+关注
关注
1文章
805浏览量
31162 -
PIN
+关注
关注
1文章
304浏览量
24284 -
Rust
+关注
关注
1文章
228浏览量
6598
原文标题:摘要
文章出处:【微信号: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上为
评论