繁体   English   中英

Rust 异步丢弃

[英]Rust Async Drop

我正面临这样一种情况,我需要从 object 的放置处理程序运行异步代码。整个应用程序在 tokio 异步上下文中运行,所以我知道放置处理程序是使用活动的 tokio 运行时调用的,但不幸的是它自己放置了是同步 function。

理想情况下,我想要一个适用于多线程和当前线程运行时的解决方案,但如果不存在,那么我可以使用阻止丢弃线程并依赖其他线程来驱动的解决方案期货。

我考虑了多种选择,但我不确定哪种方法最好或对它们的权衡了解不多。 对于这些示例,假设我的 class 有一个async terminate(&mut self) function,我想从drop()调用它。

struct MyClass;
impl MyClass {
    async fn terminate(&mut self) {}
}

选项 1: tokio::runtime::Handle::block_on

impl Drop for MyClass {
    fn drop(&mut self) {
        tokio::runtime::Handle::current().block_on(self.terminate());
    }
}

这似乎是最直接的方法,但不幸的是它会恐慌

Cannot start a runtime from within a runtime. This happens because a function (like `block_on`) attempted to block the current thread while the thread is being used to drive asynchronous tasks.

游乐场

我对此有点困惑,因为我认为Handle::block_on会使用当前正在运行的运行时,但它似乎试图启动一个新的运行时? 这里发生了什么?

此外,根据Handle::block_on的文档,这不能驱动 IO 个线程。 所以我猜想阻塞这个线程是有风险的——如果同时破坏了太多对象,每个对象都阻塞了一个线程,而那些 futures 等待 IO 工作,那么这将死锁。

选项 2: futures::executor::block_on

impl Drop for MyClass {
    fn drop(&mut self) {
        futures::executor::block_on(self.terminate());
    }
}

游乐场

似乎工作正常。 如果我理解正确,那么它会在当前线程上生成一个新的非 tokio 执行程序,并让该线程驱动未来。 这是一个问题吗? 这是否会导致正在运行的 tokio 执行者和新的期货执行者之间发生冲突?

另外,这实际上可以驱动 IO 个线程,避免选项 1 的问题吗? 还是会发生那些 IO 线程仍在等待 tokio 执行程序的情况?

选项 3: tokio::task::spawn with futures::executor::block_on

impl Drop for MyClass {
    fn drop(&mut self) {
        let task = tokio::task::spawn(self.terminate());
        futures::executor::block_on(task);
    }
}

游乐场

这应该让 tokio 运行时驱动终止 future 而 futures 运行时只阻塞当前线程等待 tokio 运行时完成? 这是否比选项 2 更安全并且导致运行时之间的冲突更少? 不幸的是,这遇到了我无法弄清楚的终生问题。:

error[E0759]: `self` has an anonymous lifetime `'_` but it needs to satisfy a `'static` lifetime requirement
   --> src/main.rs:8:44
    |
7   |     fn drop(&mut self) {
    |             --------- this data with an anonymous lifetime `'_`...
8   |         let task = tokio::task::spawn(self.terminate());
    |                                       ---- ^^^^^^^^^
    |                                       |
    |                                       ...is used here...
    |
note: ...and is required to live as long as `'static` here
   --> src/main.rs:8:20
    |
8   |         let task = tokio::task::spawn(self.terminate());
    |                    ^^^^^^^^^^^^^^^^^^
note: `'static` lifetime requirement introduced by this bound
   --> /playground/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.17.0/src/task/spawn.rs:127:28
    |
127 |         T: Future + Send + 'static,
    |                            ^^^^^^^

我也尝试使用LocalSet解决此问题,但无法正常工作。 有什么办法可以使这项工作?

选项 3b

但是,如果我让terminate()按值获取self并将MyClass包装到Wrapper中,我就能让它工作。 不漂亮但可能比选项 2 更好,因为它使用 tokio 运行时来驱动未来?

struct MyClass;
impl MyClass {
  async fn terminate(self) {}
}

struct Wrapper(Option<MyClass>);

impl Drop for Wrapper {
    fn drop(&mut self) {
        if let Some(v) = self.0.take() {
            let task = tokio::task::spawn(v.terminate());
            futures::executor::block_on(task).unwrap();
        }
    }
}

游乐场

这是一个好方法吗? tokio runtime 驱动 drop 的未来真的很重要,还是更简单的选项 2 更好? 有什么方法可以使选项 3b 更漂亮/更易于使用?

选项 4:后台任务

我在这里找到了这个选项: https://stackoverflow.com/a/68851788/829568它基本上在 object 的构造函数中生成一个后台任务,等待触发器并在触发时运行异步删除代码。 然后 drop 实现触发它并运行一个繁忙的等待循环,直到它完成。

这似乎过于复杂,而且比这里的其他选项更容易出错。 或者这实际上是最好的解决方案?

关于耗尽工作线程的附带问题

除了选项 1 之外,所有这些选项都会阻止 tokio 工作线程等待异步删除完成。 在多线程运行时,这在大多数情况下会 go 很好,但如果多个析构函数并行运行,理论上可能会锁定所有工作线程 - 而 IIUC 那么我们将陷入死锁,没有线程取得进展。 选项 1 似乎好一些,但block_on文档说它只能驱动非 IO 期货。 因此,如果太多析构函数执行 IO 工作,它仍可能会锁定。 有没有办法告诉 tokio 将工作线程的数量增加一个? 如果我们对阻塞的每个线程都这样做,是否可以避免这个问题?

选项 5:新线程中的新运行时

impl Drop for MyClass {
    fn drop(&mut self) {
        std::thread::scope(|s| {
            s.spawn(|| {
                let runtime = tokio::runtime::Builder::new_multi_thread()
                    .build()
                    .unwrap();
                runtime.block_on(self.terminate());
            });
        });
    }
}

游乐场

这似乎有效,并试图通过在新线程中的新运行时运行放置任务来避免阻塞工作线程的问题。 希望这个新线程能够驱动 IO 个任务。 但这真的能完全解决问题吗? 如果 drop 任务依赖于在主 tokio 执行器上运行的 IO 任务怎么办? 我认为这可能仍然有机会导致程序无限期锁定。

如果你想“做某事”,而不需要对 MyClass 进行独占可变访问,也许使用 oneshot 通道来触发异步计算会起作用吗? 有点类似于选项#4。

您也可以通过频道发送一些额外的 state。

use std::time::Duration;

use tokio::{
    runtime::Runtime,
    sync::oneshot::{self, Receiver, Sender},
    time::interval,
};

struct MyClass {
    tx: Option<Sender<()>>, // can have SomeStruct instead of () 
    // my_state: Option<SomeStruct>
}

impl MyClass {
    pub async fn new() -> Self {
        println!("MyClass::new()");

        let (tx, mut rx) = oneshot::channel();

        tokio::task::spawn(async move {
            let mut interval = interval(Duration::from_millis(100));

            println!("drop wait loop starting...");

            loop {
                tokio::select! {
                    _ = interval.tick() => println!("Another 100ms"),
                    msg = &mut rx => {
                        println!("should process drop here");
                        break;
                    }
                }
            }
        });

        Self { tx: Some(tx) }
    }
}

impl Drop for MyClass {
    fn drop(&mut self) {
        println!("drop()");
        self.tx.take().unwrap().send(()).unwrap();
        // self.tx.take().unwrap().send(self.my_state.take().unwrap()).unwrap(); 
    }
}

#[tokio::main]
async fn main() {
    let class = MyClass::new().await;
}

这大部分时间打印:

MyClass::new()
drop()
drop wait loop starting...
should process drop here

有时,该过程在接收方任务有机会产生之前就已经存在。 但是如果你有一个非退出代码,应该没问题。

不确定 select.interval,tick 是否必要。 尽管不幸的是 oneshot 通道没有异步阻塞接收方法。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM