4.2 `Pin`
Pin — Comprehensive Rust3 分钟阅读
译文 · 基于 Comprehensive Rust
原文链接: https://google.github.io/comprehensive-rust/concurrency/async-pitfalls/pin.html
4.2 Pin
回想一下:async 函数或块会创建一个实现 Future 并包含所有局部变量的类型。其中一些变量可能持有指向其他局部变量的引用(指针)。为确保这些引用保持有效,该 future 绝不能被移动到不同的内存位置。
为防止 future 类型在内存中移动,只能通过固定指针(pinned pointer)对其进行轮询。Pin 是对引用的包装,禁止所有会把它所指向的实例移动到不同内存位置的操作。
| |
你可能认出这是 actor 模式的例子。Actor 通常在循环中调用
select!。这是对前几课内容的综合,请慢慢消化。
天真地在
select!中加入_ = sleep(Duration::from_millis(100)) => { println!(..) }。这永远不会执行。为什么?相反,在
loop外加入包含该 future 的timeout_fut:
1 2 3 4 5 6 7 8 9 10// Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 # let timeout_fut = sleep(Duration::from_millis(100)); loop { select! { .., _ = timeout_fut => { println!(..); }, } }这样仍不行。跟随编译器错误,在
select!中给timeout_fut加上&mut以绕过移动,然后使用Box::pin:
1 2 3 4 5 6 7 8 9 10// Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 # let mut timeout_fut = Box::pin(sleep(Duration::from_millis(100))); loop { select! { .., _ = &mut timeout_fut => { println!(..); }, } }这能编译,但超时一旦到期,之后每次迭代都是
Poll::Ready(fused future 对此有帮助)。更新为每次到期时重置timeout_fut:
1 2 3 4 5 6 7 8 9 10 11 12// Copyright 2024 Google LLC // SPDX-License-Identifier: Apache-2.0 # let mut timeout_fut = Box::pin(sleep(Duration::from_millis(100))); loop { select! { _ = &mut timeout_fut => { println!(..); timeout_fut = Box::pin(sleep(Duration::from_millis(100))); }, } }
Box在堆上分配。在某些情况下,std::pin::pin!(最近才稳定,旧代码常用tokio::pin!)也是选项,但对会重新赋值的 future 很难用。另一替代方案是完全不用
pin,而是再派生一个任务,每 100ms 向oneshot通道发送一次。包含指向自身指针的数据称为自引用(self-referential)。通常,Rust 借用检查器会阻止自引用数据被移动,因为引用不能比它们指向的数据活得更久。然而,async 块与函数的代码转换未经借用检查器验证。
Pin是对引用的包装。不能通过固定指针把对象从其所在位置移走。但仍可通过未固定指针移动它。
Futuretrait 的poll方法使用Pin<&mut Self>而不是&mut Self来引用实例。这就是为什么只能在固定指针上调用它。