【问题标题】:Abortable: Dangling Futures?Abortable:悬而未决的期货?
【发布时间】:2023-02-16 14:22:00
【问题描述】:

我正在使用 Abortable crate 来暂停 Future 的执行。假设我有一个失败的未来,其中异步函数本身等待其他异步函数。我的问题是,如果我中止根 Future,子 Futures 会同时立即中止,还是会悬空?

我阅读了Abortable的源代码,特别是try_poll的代码:

fn try_poll<I>(
        mut self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        poll: impl Fn(Pin<&mut T>, &mut Context<'_>) -> Poll<I>,
    ) -> Poll<Result<I, Aborted>> {
        // Check if the task has been aborted
        if self.is_aborted() {
            return Poll::Ready(Err(Aborted));
        }

        // attempt to complete the task
        if let Poll::Ready(x) = poll(self.as_mut().project().task, cx) {
            return Poll::Ready(Ok(x));
        }

        // Register to receive a wakeup if the task is aborted in the future
        self.inner.waker.register(cx.waker());

        // Check to see if the task was aborted between the first check and
        // registration.
        // Checking with `is_aborted` which uses `Relaxed` is sufficient because
        // `register` introduces an `AcqRel` barrier.
        if self.is_aborted() {
            return Poll::Ready(Err(Aborted));
        }

        Poll::Pending
    }

我的理解是,一旦abort被调用,它就会传播到下游 Futures,即当根 Future 中止时,它将停止轮询其子 Future(因为 Poll::Ready(Err(Aborted)) 将被返回),这将依次停止轮询它的孩子。如果这个推理是正确的,那么调用 abort 的效果是立竿见影的。
另一个论点是,如果 Future 是基于拉取的,则应首先调用根节点,然后传播到子任务,直到叶节点被调用并中止(然后返回到根节点)。这意味着在调用 abort 方法与叶 Future 实际停止轮询之间存在延迟。可能是相关的,但是这个blogpost 提到了悬而未决的任务,我担心是这种情况。
例如,这是我写的一个玩具示例:

use futures::future::{AbortHandle, Abortable};
use tokio::{time::sleep};
use std::{time::{Duration, SystemTime}};

/*
 *       main
 *         \
 *       child
 *         | \
 *        |   \
 *    leaf1   leaf2
 */
 

async fn leaf2() {
    println!("This will not be printed")
}

async fn leaf1(s: String) {
    println!("[{:?}] ====== in a ======", SystemTime::now());
    for i in 0..100000 {
        println!("[{:?}] before sleep i is {}", SystemTime::now(), i);
        sleep(Duration::from_millis(1)).await;
        println!("[{:?}] {}! i is {}", SystemTime::now(), s.clone(), i);
    }
}
 
async fn child(s: String) {
    println!("[{:?}] ====== in child ======", SystemTime::now());
    leaf1(s.clone()).await;
    leaf2().await
}
 
#[tokio::main]
async fn main() {
    let (abort_handle, abort_registration) = AbortHandle::new_pair();
    let result_fut = Abortable::new(child(String::from("Hello")), abort_registration);

    tokio::spawn(async move {
        println!("{:?} ^^^^^ before sleep ^^^^^", SystemTime::now());
        sleep(Duration::from_millis(100)).await;
        println!("{:?} ^^^^^ after sleep, about to abort ^^^^^", SystemTime::now());
        abort_handle.abort();
        println!("{:?} ***** operation aborted *****", SystemTime::now());
    });

    println!("{:?} ====== before main sleeps ======", SystemTime::now());
    sleep(Duration::from_millis(5)).await;
    println!("{:?} ====== after main wakes up from sleep and now getting results \
            ======", SystemTime::now());
    result_fut.await.unwrap();
}

Rust playground
我个人更倾向于第一个论点,即根的流产和叶的流产之间没有延迟,因为叶不需要知道它需要中止(叶仅在根告诉它时才拉动)。上面的例子打印了child被执行的时间和root被中止的时间。 child 的执行总是在 root 被中止之前,但是我不确定这是否可以证明我的第一个论点是正确的,所以我想知道大家的想法!

【问题讨论】:

    标签: rust async-await rust-tokio


    【解决方案1】:

    是的,因为 future 需要轮询才能执行,但如果中止则不会轮询,子 futures 也不会被轮询,因此执行将立即停止。

    当然,执行只会在到达下一个屈服点后停止,使用tokio::spawn() 生成的任务不会停止。

    【讨论】:

    • 谢谢你! (如果我有足够的声望点,我会赞成你的回答)为了确保我真的理解,“只有在达到下一个屈服点后才会停止执行”,你的意思是直到下一次它试图轮询未来? (如果它已经在轮询孩子未来,那么进程不会立即终止)我的下一个问题是,当我们在根上调用abort时,丢弃是如何发生的?它是否遵循基于拉动的中止链,或者只是根节点将被中止而其他下游期货将被悬挂?
    • @ZHEng 我的意思是,如果它当前正在执行 println!(),它不会立即停止,只有在到达 sleep() 或类似的东西时才会停止。我不明白你的第二个问题。
    • 如果在leaf1函数中省略sleep(),然后在child上调用abort,child是否会停止从leaf1进行轮询并停止打印?我想我的第二个问题是,当调用 abort 时,drop 是如何发生的?未来是否按顺序下降(例如从根到叶,或从叶到根,或只是根)?当调用 abort 时,它们会掉线吗?
    • @ZHEng 如果你要取消睡眠,child 将永远不会屈服,因此即使你中止它也永远不会停止。如果 future 的所有者以及何时将其丢弃,则丢弃 future 将发生,与中止无关。
    • sleep 是child 屈服的唯一途径吗?还有其他办法吗?
    【解决方案2】:
    1. 删除 Future 类型等于删除该 Future 中的所有状态,包括其子 Future。记住 Rust 中的 Futures 都是状态机。如果你成功地安全地删除了它的状态,那么这意味着所有的执行必须在删除之前已经停止,否则就是数据竞争。

    2. 具体来说,删除一个 tokio JoinHandle 在概念上只会删除句柄本身,但不会对句柄所代表的任务做任何事情。或者换句话说,如果您使用 tokio::spawn(),那么您投入该任务的任何 Future 都与您当前的 Future 无关(除非您可以从 JoinHandle 收到返回结果并明确调用中止)。所以他们默认将 tokio 的任务称为分离的。

    3. 让我们谈谈时间问题。对于Abortable,它不会直接“丢弃”你的Future。相反,它以间接的方式进行。如果你调用AbortHandle::abort(),然后立即翻转一个原子布尔标志,然后调用Waker::wake(),但是异步执行器此时此刻不会意识到这一点.执行者仍会认为您的Abortable此时正在处理或暂停。我们应该分开讨论它们:

      1. 在原子布尔翻转时,执行者仍在轮询您的Future。然后根据source code

        1. 如果此轮询恰好是完成未来的轮询,则 Abortable 将返回完成。
        2. 如果此轮询最终返回Pending,则Abortable 很有可能返回中止错误,返回悬念的可能性很小,具体取决于原子标志翻转何时可观察到此轮询线程.

          如果返回完成或中止错误,Abortable 将被视为已完成并最终被删除。当它被丢弃时,这意味着它的所有孩子 Futures 都已经被丢弃了。

          如果您碰巧以非常糟糕的风格进行编码并且此轮询恰好触及了长时间的计算/阻塞调用,那么不幸的是,其他一切都必须等待此轮询返回的时间,而执行程序可能会在此过程中饿死。

        3. 您的Future 在原子布尔翻转时被暂停。 Waker::wake() 调用告诉执行者在未来的某个时间至少轮询一次 Abortable。一段时间后,执行人将最终决定对您的Abortable进行投票。然后几乎立即轮询返回一个中止的错误。然后同样的事情发生了,你的 Abortable 作为 Future 将被视为完成并最终被丢弃。

          总之,不,中止不会与下降同时发生。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-12-10
      • 1970-01-01
      • 2018-11-28
      • 2017-05-06
      • 2022-01-03
      • 2021-09-30
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多