Rust中Pin/Unpin详解

有些事情你总是学了又忘记(或者说你从来就没学过?)
对我来说,其中之一就是在rust中 pin/unpin 。
每次我读到有关固定的解释,我的大脑就像 ,几周后就像 。
所以,我写这篇文章是为了强迫我的大脑记住这些知识。我们看看效果如何! pin pin 是一种指针,可以看作是 &mut t 和 &t 之间的折中。pin 的重点是说: 这个值可以被修改(就像 &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 or poll them#[warn(unused_must_use)] on by default – rustc 这是因为调用 self_ref 实际上什么都不做, 我们实际上会得到类似于: struct selfreffuture { state: selfreffuturestate, v: maybeuninit, 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 (假设它们不需要移动该值)。 一个微小的例子 这里不需要固定: 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, ...) -> ... { ... }
当我们 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`.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’s pin! 是这样实现的: // 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`.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 和 &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, _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; }


ARM7芯片实现配电综合测控仪的应用方案
首款可编程忆阻器AI计算机研发成功
瑞典禁止举行5G频谱拍卖的企业使用华为和中兴的产品
利维能洪树:26700电池取道轻型车市场
如何选择适合儿童电子琴的MP3芯片方案?
Rust中Pin/Unpin详解
智能音箱控制智能家居的工作原理
自动驾驶中基于图搜索的常用路径规划算法介绍
明年OLED市场规模将达192亿美元 中日韩厂商竞争加剧
什么是差分晶振 差分晶振的优势 差分输出与单端输出的差别
F103程序移植到F407的经验小结
一文详解倍压整流电路
陶瓷电容在智能手机中有什么作用呢?
激光焊接技术在焊接0.2mm铝合金的工艺
低钴及无钴化电池即将成为下一代动力电池的方向
维修过程中,电源开关不小心爆炸啦!
如何在Go中操作文本文件
超详细的机械设计基础知识点
工信部印发工业互联网标识管理办法
旅行者一号、二号距地球近200亿公里——靠什么传回地球信号?