提问人:LittleBoxOfSunshine 提问时间:7/17/2023 最后编辑:LittleBoxOfSunshine 更新时间:7/19/2023 访问量:119
等同于 C++ 的规范 Rust 在闭包中捕获“this”
Canonical Rust equivalent to C++ capture `this` in closure
问:
我很难找到与此类代码等效的代码,这让我怀疑它不是 Rust 的惯用代码,但目前尚不清楚规范方法是什么,因为我找不到所讨论问题的实例。
请考虑以下结构:
struct Looper {
shared_value: Arc<AtomicU64>,
handle: Option<JoinHandle<()>>
}
impl Looper {
pub fn new(task: Box<dyn Fn() + Send>) -> Self {
let mut looper = Self { shared_value: Arc::new(AtomicU64::new(10)), handle: None };
let shared_state = looper.shared_value.clone();
looper.handle = Some(thread::spawn(move || {
for _ in 0..1000 {
sleep(Duration::from_millis(shared_state.load(Relaxed)));
task();
}
}));
looper
}
pub fn set_value(&self, value: u64) {
self.shared_value.store(value, Relaxed)
}
}
impl Drop for Looper {
fn drop(&mut self) {
self.handle.take().unwrap().join().unwrap();
}
}
考虑一个非常人为的问题,我们想要一个可以重用结构的包装器对象,但它在包装器内部管理对睡眠周期的更改。
在 C++ 中,这很简单,只需在 lambda 中捕获:this
class Wrapper {
private:
std::unique_ptr<Looper> looper;
public:
Wrapper(Duration pollingRate, const std::function<uint64_t()>& task) {
looper = std::make_unique<Looper>( [this, task]() { looper->set_value(task()); });
}
}
在 Rust 中,有两个问题。首先,没有等效的构造函数来捕获。该类型的问题可以通过使用选项来解决。我可以先创建一个元素,然后尝试创建一个闭包并捕获当前设置为例如this
None
None
struct WrappedLooper {
looper: Option<Looper>
}
impl WrappedLooper {
pub fn new(task: Box<dyn Fn() -> u64 + Send>) -> Self {
let mut wrapped = WrappedLooper { looper: None };
let this = &wrapped;
wrapped.looper = Some(Looper::new(Box::new(move || {
this.looper.unwrap().set_value(task());
})));
wrapped
}
}
这显然是行不通的,因为你不能搬出。我可以将内部状态切换到 Arc 并互斥它,但这对我来说似乎非常沉重且不正确:this
struct WrappedLooper {
looper: Arc<Mutex<Option<Looper>>>
}
impl WrappedLooper {
pub fn new(task: Box<dyn Fn() -> u64 + Send>) -> Self {
let mut wrapped = WrappedLooper { looper: Arc::new(Mutex::new(None)) };
let copy = wrapped.looper.clone();
wrapped.looper.lock().unwrap().replace(Looper::new(Box::new(move || {
copy.lock().unwrap().as_mut().as_ref().unwrap().set_value(task());
})));
wrapped
}
}
所以我想知道的是,在访问始终安全的场景中,进行这种自引用初始化(选项或其他东西)的正确方法是什么?(这里是安全的,因为对象的寿命比闭包长 + 共享的可变状态是原子的)
答:
据我了解,有两个独立的问题。一个与生成的线程的生命周期有关;另一种是自我参照。
让我们通过完全忽略线程来忽略第一个问题。首先,让我们尝试只要求闭包在以下时间内存在:Looper::new
pub fn new<'a>(_task: Box<dyn Fn() + Send + Sync + 'a>) -> Self {
Self {
shared_value: Arc::new(AtomicU64::new(10)),
}
}
然后,其他一切都编译得很好。但这并不是很有用:如果我们不能保证函数存在过去,我们显然不能在线程中使用它。相反,我们可以告诉 Rust 它必须与被创建的时间一样长:Looper::new
task
Looper
struct Looper<'a> {
shared_value: Arc<AtomicU64>,
_marker: PhantomData<dyn Fn() + Send + Sync + 'a>,
}
impl<'a> Looper<'a> {
pub fn new(_task: Box<dyn Fn() + Send + Sync + 'a>) -> Self {
Self {
shared_value: Arc::new(AtomicU64::new(10)),
_marker: PhantomData,
}
}
pub fn set_value(&self, value: u64) {
self.shared_value.store(value, Relaxed)
}
}
然后,眼前的问题是,我们希望有一个依赖于对自身的引用的实例。我们不能随心所欲地这样做:如果这种自我参照被移动了,会发生什么?因此,完成这项工作需要一些不安全的 Rust 使用 ,或者至少是一个封装它的库。WrappedLooper
Looper
Looper
Looper
Pin
然而,即使实现了这一点,我们也遇到了另一个问题:没有办法真正知道线程的寿命有多长。让我们重新添加生成的线程:
11 | impl<'a> Looper<'a> {
| -- lifetime `'a` defined here
12 | pub fn new(task: Box<dyn Fn() + Send + Sync + 'a>) -> Self {
| ---- `task` is a reference that is only valid in the associated function body
...
20 | / thread::spawn(move || {
21 | | sleep(Duration::from_millis(shared_state.load(Relaxed)));
22 | | task();
23 | | });
| | ^
| | |
| |__________`task` escapes the associated function body here
| argument requires that `'a` must outlive `'static`
(游乐场链接)
告诉 Rust 我们的函数存在的时间不够长:它必须是 .这是有道理的:考虑一下如果在延迟结束之前被丢弃会发生什么。然后,无法再保留对 looper 的引用。C++代码段正好有这个问题:如果包装器
被销毁,那么在Looper
的任务
中捕获的this
现在悬空。Looper
'static
WrappedLooper
task
因为 Rust 没有提供一种方法来保证 looper 实例确实比线程活得更久,所以最好在这里只使用引用计数。
旁注:
在 Rust 中,有两个问题。首先,没有等效的构造函数来捕获。该类型的问题可以通过使用选项来解决。
this
第一个问题也存在于 C++ 版本中,只是隐藏了。在 C++ 中,可以为 null(并以 null 开头)。因此,它就像 ,其中在 C++ 中基本上等同于 Rust。不同之处在于 Rust 迫使我们明确地识别它。unique_ptr
Option
looper->set_value
looper.unwrap_unchecked().set_value
评论
'static
std::thread::JoinGuard
这种问题可以通过 Arc::new_cyclic()
来解决:
struct WrappedLooper {
looper: Arc<Looper>,
}
impl WrappedLooper {
pub fn new(task: Box<dyn Fn() -> u64 + Send>) -> Self {
let looper = Arc::<Looper>::new_cyclic(|looper| {
let looper = Weak::clone(looper);
Looper::new(Box::new(move || {
let looper = looper.upgrade().expect("looper gone");
looper.set_value(task());
}))
});
WrappedLooper { looper }
}
}
但是,此代码存在一个问题,该问题也存在于您的 C++ 代码和 Rust 代码中(在两个 Rust 代码段中它可能会导致恐慌,在 C++ 代码段中它可能会导致 UB):如果线程在我们完成构造之前开始运行,我们将看到一个 null(这将导致在您的 Rust 代码段或我的 Rust 代码段中出现恐慌, 和 C++ 中的 UB,因为我们将尝试取消引用空指针)。这可以通过通道来解决:Mutex
Looper
Looper
unwrap()
upgrade().expect()
impl WrappedLooper {
pub fn new(task: Box<dyn Fn() -> u64 + Send>) -> Self {
let (construction_completed_tx, construction_completed_rx) = mpsc::sync_channel(1);
let looper = Arc::<Looper>::new_cyclic(|looper| {
let looper = Weak::clone(looper);
Looper::new(Box::new(move || {
construction_completed_rx.recv().expect("looper gone");
let looper = looper.upgrade().expect("looper gone");
looper.set_value(task());
}))
});
construction_completed_tx.send(()).unwrap();
WrappedLooper { looper }
}
}
评论