等同于 C++ 的规范 Rust 在闭包中捕获“this”

Canonical Rust equivalent to C++ capture `this` in closure

提问人:LittleBoxOfSunshine 提问时间:7/17/2023 最后编辑:LittleBoxOfSunshine 更新时间:7/19/2023 访问量:119

问:

我很难找到与此类代码等效的代码,这让我怀疑它不是 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 中,有两个问题。首先,没有等效的构造函数来捕获。该类型的问题可以通过使用选项来解决。我可以先创建一个元素,然后尝试创建一个闭包并捕获当前设置为例如thisNoneNone

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

所以我想知道的是,在访问始终安全的场景中,进行这种自引用初始化(选项或其他东西)的正确方法是什么?(这里是安全的,因为对象的寿命比闭包长 + 共享的可变状态是原子的)

防锈 封盖

评论

0赞 Chayim Friedman 7/17/2023
我相信你的 C++ 代码有一个数据竞争(除了我在回答中谈到的错误)。这恰恰说明了编写正确的并发程序是多么困难,以及为什么需要 Rust......
0赞 LittleBoxOfSunshine 7/19/2023
同意防锈钻头:)当我进行最小的重现时,我将其做得太小,并且没有在丢弃中捕获连接句柄。让我知道这在 rust 中是否仍然是错误的,但目的是将线程视为孩子,我们等待它退出后再放弃 Looper
0赞 BrownieInMotion 7/19/2023
在下面的讨论中提到了这一点,但在这里复制: 我相信标准库中没有工具可以告诉 Rust 你的结构将比线程更长,即使你在 drop 时加入:github.com/rust-lang/rust/issues/24292

答:

4赞 BrownieInMotion 7/17/2023 #1

据我了解,有两个独立的问题。一个与生成的线程的生命周期有关;另一种是自我参照。

让我们通过完全忽略线程来忽略第一个问题。首先,让我们尝试只要求闭包在以下时间内存在:Looper::new

    pub fn new<'a>(_task: Box<dyn Fn() + Send + Sync + 'a>) -> Self {
        Self {
            shared_value: Arc::new(AtomicU64::new(10)),
        }
    }

然后,其他一切都编译得很好。但这并不是很有用:如果我们不能保证函数存在过去,我们显然不能在线程中使用它。相反,我们可以告诉 Rust 它必须与被创建的时间一样长:Looper::newtaskLooper

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 使用 ,或者至少是一个封装它的库。WrappedLooperLooperLooperLooperPin

然而,即使实现了这一点,我们也遇到了另一个问题:没有办法真正知道线程的寿命有多长。让我们重新添加生成的线程:

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'staticWrappedLoopertask

因为 Rust 没有提供一种方法来保证 looper 实例确实比线程活得更久,所以最好在这里只使用引用计数。


旁注:

在 Rust 中,有两个问题。首先,没有等效的构造函数来捕获。该类型的问题可以通过使用选项来解决。this

第一个问题也存在于 C++ 版本中,只是隐藏了。在 C++ 中,可以为 null(并以 null 开头)。因此,它就像 ,其中在 C++ 中基本上等同于 Rust。不同之处在于 Rust 迫使我们明确地识别它。unique_ptrOptionlooper->set_valuelooper.unwrap_unchecked().set_value

评论

0赞 LittleBoxOfSunshine 7/19/2023
感谢您的详尽回答!不幸的是,您确实让我意识到在将有问题的实际代码转换为较小的重现时我犯了一个错误。在最初的假设中,肯定有一个悬而未决的指针问题,我已经解决了。该线程并不打算比 Looper 活得更久。在 Looper drop 加入线程的范式中,不再有悬空引用。WrappedLooper 拥有 Looper,它拥有一个线程句柄,它拥有闭包,Task 拥有对 Looper 的引用。由于我们等待线程,那棵树应该可以正常地走回去。
0赞 LittleBoxOfSunshine 7/19/2023
在这种情况下,是否值得尝试正确生存期(作为 Rust 初学者,这是一个薄弱领域,我会先自己尝试更多),还是借款检查器永远不会满足于这种类型的所有权链?当我编写可以用 C++ 安全编写的 Rust 代码时,我试图记住: 1. 有时我会犯错误 2.我更熟悉 C++,所以我通常可以编写安全的代码,在 rust 中,因为它对我来说是新的,我比 C++ 3 更有可能搞砸不安全的 rust。很多时候,“需要”不安全实际上只是“我还不知道如何在生锈中做到这一点”
0赞 LittleBoxOfSunshine 7/19/2023
我认为从 C++ 思维模式转换最困难的部分是,在 Rust 中,我可以通过引用计算所有内容来“作弊”,即使它并不总是必要的,而且很难说我什么时候应该尝试在生命周期注释方面做得更好与否,这里引用计数真的更好,因为次要优化不值得不安全的块
0赞 BrownieInMotion 7/19/2023
不幸的是,在 Rust 中,你可能会卡在引用计数上。我不认为有办法让一生成功,即使它可能是安全的。除非您使用作用域线程(此处不起作用),否则除了将其与 Looper 关联之外,没有办法让闭包的生存期成为任何东西。曾经有过,但在第一个稳定版本之前就被砍掉了。'staticstd::thread::JoinGuard
0赞 Chayim Friedman 7/17/2023 #2

这种问题可以通过 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,因为我们将尝试取消引用空指针)。这可以通过通道来解决:MutexLooperLooperunwrap()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 }
    }
}