如果间隔计时器事件始终准备就绪,为什么 AnyEvent::child 永远不会 运行 回调?

Why won't AnyEvent::child callbacks ever run if interval timer events are always ready?

更新 此问题可以使用 https://github.com/zbentley/AnyEvent-Impl-Perl-Improved/tree/io-starvation

中的修复程序解决

上下文:

我正在将 AnyEvent 与一些 otherwise-synchronous 代码集成。同步代码需要安装一些观察者(在定时器、child 进程和文件上),等待至少一个观察者完成,做一些 synchronous/blocking/legacy 的事情,然后重复。

我正在使用基于 pure-perl AnyEvent::Loop 的事件循环,这对我目前的目的来说已经足够了;我需要它的大部分功能是 signal/process/timer 跟踪。

问题:

如果我有一个可以暂时阻塞事件循环的回调,child-process-exit events/callbacks 永远不会触发。最简单的例子我可以让 watches 成为一个 child 进程,而 运行s 成为一个间隔计时器。间隔计时器在完成之前做了一些阻塞:

use AnyEvent;

# Start a timer that, every 0.5 seconds, sleeps for 1 second, then prints "timer":
my $w2 = AnyEvent->timer(
    after => 0,
    interval => 0.5,
    cb => sub {
        sleep 1; # Simulated blocking operation. If this is removed, everything works.
        say "timer";
    },
);

# Fork off a pid that waits for 1 second and then exits:
my $pid = fork();
if ( $pid == 0 ) {
    sleep 1;
    exit;
}

# Print "child" when the child process exits:
my $w1 = AnyEvent->child(
    pid => $pid,
    cb => sub {
        say "child";
    },
);

AnyEvent->condvar->recv;

此代码使 child 进程陷入僵局,并一遍又一遍地打印 "timer",持续了 "ever"(我 运行 它打印了几分钟)。如果从计时器的回调中删除 sleep 1 调用,代码将正常工作并且 child 进程观察器会按预期触发。

我希望 child 观察者最终 运行(在 child 退出后的某个时刻,以及事件队列 运行 中的任何间隔事件,阻塞,并完成),但它没有。

sleep 1可以是任何阻塞操作。它可以替换为 busy-wait 或任何其他需要足够长的东西。它甚至不需要花一秒钟;它似乎只需要 a) 在 child-exit event/SIGCHLD 交付期间 运行ning,以及 b) 根据挂钟,导致间隔始终归因于 运行。

问题:

为什么 AnyEvent 从来没有 运行宁我的 child-process 观察者回调?

如何将 child-process-exit 事件与可能阻塞很长时间以至于下一个间隔到期的间隔事件复用?

我试过的:

我的理论是,由于在事件循环之外花费的时间而变成 "ready" 的计时器事件可以在某处无限期地 pre-empt 其他类型的就绪事件(如 child 进程观察者)在 AnyEvent 里面。我已经尝试了一些事情:

间隔是每次定时器回调开始之间的时间,即不是回调结束和下一次回调开始之间的时间。您设置了一个间隔为 0.5 的计时器,计时器的动作是休眠一秒钟。这意味着一旦定时器被触发,它将立即一次又一次地被触发,因为间隔总是在定时器返回后结束。

因此,根据事件循环的实现,可能会发生没有其他事件将被处理的情况,因为它正忙于 运行一遍又一遍地使用同一个计时器。我不知道您使用的是哪个底层事件循环(检查 $AnyEvent::MODEL),但是如果您查看 AnyEvent::Loop 的源代码(纯 Perl 实现的循环,即模型是 AnyEvent::Impl::Perl) 你会发现下面的代码:

   if (@timer && $timer[0][0] <= $MNOW) {
      do {
         my $timer = shift @timer;
         $timer->[1] && $timer->[1]($timer);
      } while @timer && $timer[0][0] <= $MNOW;

如您所见,只要有需要 运行 的定时器,它就会忙于执行定时器。通过设置间隔 (0.5) 和计时器的行为(休眠一秒),总会有一个计时器需要执行。

如果您改为更改计时器,以便通过将间隔设置为大于阻塞时间(例如 2 秒而不是 0.5 秒)来为其他事件的处理留出实际空间,则一切正常:

...
interval => 2,
cb => sub {
    sleep 1; # Simulated blocking operation. Sleep less than the interval!!
    say "timer";


...
timer
child
timer
timer

更新 可以使用 https://github.com/zbentley/AnyEvent-Impl-Perl-Improved/tree/io-starvation

中的修复程序解决此问题

@steffen-ulrich 的回答是正确的,但指出了 AnyEvent 中一个非常有缺陷的行为:由于没有底层事件队列,某些总是报告 "ready" 的事件可以无限期地抢占其他事件。

解决方法如下:

对于由于事件循环外发生的阻塞操作而始终 "ready" 的间隔计时器,可以通过将间隔调用链接到事件的下一个 运行 来防止饥饿循环,像这样:

use AnyEvent;

sub deferred_interval {
    my %args = @_;
    # Some silly wrangling to emulate AnyEvent's normal
    # "watchers are uninstalled when they are destroyed" behavior:
    ${$args{reference}} = 1;
    $args{oldref} //= delete($args{reference});
    return unless ${$args{oldref}};

    AnyEvent::postpone {
        ${$args{oldref}} = AnyEvent->timer(
            after => delete($args{after}) // $args{interval},
            cb => sub {
                $args{cb}->(@_);
                deferred_interval(%args);
            }
        );
    };

    return ${$args{oldref}};
}

# Start a timer that, at most once every 0.5 seconds, sleeps
# for 1 second, and then prints "timer":
my $w1; $w1 = deferred_interval(
    after => 0.1,
    reference => $w2,  
    interval => 0.5,
    cb => sub {
        sleep 1; # Simulated blocking operation.
        say "timer";
    },
);

# Fork off a pid that waits for 1 second and then exits:
my $pid = fork();
if ( $pid == 0 ) {
    sleep 1;
    exit;
}

# Print "child" when the child process exits:
my $w1 = AnyEvent->child(
    pid => $pid,
    cb => sub {
        say "child";
    },
);

AnyEvent->condvar->recv;

使用该代码,子进程观察器将或多或少地按时触发,并且间隔将继续触发。权衡是每个间隔计时器只会在每个阻塞回调完成后 启动 。给定 I 的间隔时间和 B 的阻塞回调 运行 时间,此方法将大约每 I + B 秒触发一次间隔事件,而先前的方法来自问题将花费 min(I,B) 秒(以潜在的饥饿为代价)。

我认为,如果 AnyEvent 有一个后备队列(许多常见的事件循环采用这种方法来防止完全像这种情况),或者如果实现 AnyEvent::postpone安装了一个类似 "NextTick" 的事件发射器,只有在检查完所有其他发射器的事件后才会触发。