3

更新此问题可以使用https://github.com/zbentley/AnyEvent-Impl-Perl-Improved/tree/io-starvation中的修复程序来解决

语境:

我正在将 AnyEvent 与一些其他同步代码集成。同步代码需要安装一些观察者(在计时器、子进程和文件上),等待至少一个观察者完成,做一些同步/阻塞/遗留的事情,然后重复。

我正在使用基于纯 perlAnyEvent::Loop的事件循环,这对于我的目的来说已经足够了;我需要它的大部分是信号/过程/定时器跟踪。

问题:

如果我有一个可以暂时阻止事件循环的回调,则子进程退出事件/回调永远不会触发。最简单的例子,我可以让手表成为一个子进程并运行一个间隔计时器。间隔计时器在完成之前会做一些阻塞:

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;

这段代码让子进程变得僵化,并一遍又一遍地打印“计时器”,“永远”(我运行了几分钟)。如果sleep 1从计时器的回调中删除调用,则代码可以正常工作,并且子进程观察程序会按预期触发。

我希望孩子观察者最终会运行(在孩子退出后的某个时刻,事件队列中的任何间隔事件都会运行、阻塞和完成),但事实并非如此。

sleep 1可以是任何阻塞操作。它可以用忙碌等待或任何其他需要足够长时间的东西来代替。它甚至不需要花一秒钟;它似乎只需要 a) 在子退出事件/SIGCHLD 传递期间运行,并且 b) 导致间隔始终要根据挂钟运行。

问题:

为什么 AnyEvent 从来没有运行我的子进程观察者回调?

如何将子进程退出事件与可能阻塞很长时间以致下一个间隔到期的间隔事件多路复用?

我试过的:

我的理论是,由于在事件循环之外花费的时间而变得“就绪”的计时器事件可以无限期地抢占 AnyEvent 内某处的其他类型的就绪事件(如子进程观察者)。我尝试了几件事:

  • 使用AnyEvent::Strict不会以任何方式显示任何错误或改变行为。
  • 部分解决方案:在任何时候删除间隔事件确实会使子进程观察程序触发(好像在 AnyEvent 内部完成了一些内部事件轮询/队列填充,只有当没有计时器事件根据挂钟“准备好”时才会发生)。缺点:在一般情况下不起作用,因为我必须知道我的子进程何时退出才能知道何时推迟我的间隔,这是重言式的。
  • 部分解决方案:与子进程观察者不同,其他间隔计时器似乎能够相互多路复用,所以我可以waitpid在另一个间隔计时器中安装手动调用来检查和获取子进程。缺点:子等待可以人为延迟(我的用例涉及大量频繁的进程创建/销毁),任何已安装并成功触发的AnyEvent::child观察者将自动收割子,而不告诉我的间隔/waitpid计时器,需要编排,和通常感觉就像我在滥用 AnyEvent。
4

2 回答 2

2

间隔是每个计时器回调开始之间的时间,即不是回调结束和下一个回调开始之间的时间。您设置了一个间隔为 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
于 2016-06-12T16:06:17.637 回答
0

更新此问题可以使用https://github.com/zbentley/AnyEvent-Impl-Perl-Improved/tree/io-starvation中的修复程序来解决

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

这是一种解决方法:

对于由于事件循环之外发生的阻塞操作而始终“准备就绪”的间隔计时器,可以通过将间隔调用链接到事件循环的下一次运行来防止饥饿,如下所示:

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::postpone我认为,如果 AnyEvent 有一个后备队列(许多常见的事件循环采用这种方法来防止与这种情况完全相同的情况),或者如果安装了一个类似“NextTick”的事件发射器的实现,则可以避免这里的很多麻烦。只有在检查了所有其他发射器的事件后才会触发。

于 2016-06-15T20:22:08.457 回答