可中止:危险的期货



我正在使用Abortable机箱来挂起Future的执行。假设我有一个可中止的未来,其中异步函数本身等待其他异步函数。我的问题是,如果我中止根Future,子Future会同时立即中止吗,还是它们会悬空?

我阅读了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))将被返回),而子Future又将停止轮询它的子Future。如果这个推理是真的,那么调用中止的效果是立竿见影的
另一个参数是,如果Future是基于拉的,则应首先调用根节点,然后传播到子任务,直到叶节点被调用并中止(然后返回根节点)。这意味着在调用abort方法和叶Future实际停止轮询之间存在延迟。可能是相关的,但这篇博客文章提到了悬而未决的任务,我担心情况就是这样
例如,这里有一个我写的玩具示例:

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();
}

铁锈游乐场
我个人更倾向于第一个论点,即根流产和叶流产之间没有延迟,因为叶不需要知道它需要流产(只有当根告诉它需要流产时,叶才会拉动)。上面的示例打印执行子进程的时间和中止根进程的时间。处决孩子总是在根源流产之前,但我不确定这是否能证明我的第一个论点是真的,所以我想知道你们怎么想!

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

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

  1. 删除一个Future类型等于删除该Future中的所有状态,包括其子Futures。请记住,Rust中的Futures都是状态机。如果您成功地安全地删除了Future的状态,那么这意味着在删除之前执行必须已经停止,否则这就是一场数据竞赛。

  2. 具体来说,从概念上讲,删除一个tokioJoinHandle只会删除句柄本身,但对句柄所代表的任务没有任何作用。或者换句话说,如果您使用tokio::spawn(),那么您在该任务中投入的任何Future都与您当前的Future无关(除非您可以从JoinHandle接收回结果并显式调用abort)。因此,他们默认情况下将tokio的任务称为分离任务。

  3. 让我们谈谈时间问题。对于Abortable;drop";您的Future。相反,它";滴状物";以间接的方式。如果调用AbortHandle::abort(),则会立即翻转一个原子布尔标志,然后调用Waker::wake(),但异步执行器此时不会意识到这一点。遗嘱执行人仍然会认为你的Abortable目前正在进行或暂停。我们应该单独讨论:

    1. 执行器在原子布尔翻转时仍在轮询Future。然后根据源代码

      1. 如果这个轮询恰好是完成未来的轮询,那么Abortable将返回一个完成
      2. 如果该轮询最终返回Pending,则Abortable返回中止错误的可能性很高,返回到悬念的可能性很小,这取决于原子标志翻转何时对该轮询的线程可见

      如果返回完成或中止错误,则Abortable作为Future将被视为已完成,并最终(*)被丢弃。当它被丢弃时,这意味着它的所有子Future也将被丢弃。

      如果您碰巧以一种非常糟糕的风格进行编码,而此轮询恰好碰到了一个长计算/阻塞调用,那么不幸的是,无论此轮询返回多长时间,其他所有操作都必须等待,并且执行器可能在这个过程中缺乏。

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

    总之,不,中止不会与丢弃同时发生。

*:我不确定Future在完成后什么时候会被丢弃。这实际上取决于Rust编译器如何转换.await点,我还没有研究过。

最新更新