Perl 为什么赢了';t AnyEvent::如果间隔计时器事件始终准备就绪,子回调是否会运行?

Perl 为什么赢了';t AnyEvent::如果间隔计时器事件始终准备就绪,子回调是否会运行?,perl,events,timer,waitpid,anyevent,Perl,Events,Timer,Waitpid,Anyevent,更新使用中提供的修复程序可以解决此问题 上下文: 我正在将AnyEvent与一些其他同步代码集成。同步代码需要安装一些观察程序(在计时器、子进程和文件上),等待至少一个观察程序完成,执行一些同步/阻塞/遗留操作,然后重复 我使用的是纯perlAnyEvent::Loop的事件循环,这对于我现在的目的来说已经足够好了;我最需要的是信号/进程/计时器跟踪 问题: 如果我有一个回调可以暂时阻止事件循环,那么子进程退出事件/回调永远不会触发。我能做的最简单的例子是监视一个子进程并运行一个间隔计时器。间隔

更新使用中提供的修复程序可以解决此问题

上下文:

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

我使用的是纯perl
AnyEvent::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)导致间隔始终根据wallclock运行

问题:

为什么没有任何事件运行我的子进程观察回调

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

我所尝试的:

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

  • 使用
    AnyEvent::Strict
    不会暴露任何错误或以任何方式更改行为
  • 部分解决方案:在任何点删除间隔事件都会触发子进程观察器(就好像在任何事件中都有一些内部事件轮询/队列填充,只有在没有计时器事件根据wallclock“就绪”时才会发生)。缺点:在一般情况下,这不起作用,因为我必须知道我的孩子进程何时退出,才能知道何时推迟我的间隔,这是重复的
  • 部分解决方案:与子进程观察器不同,其他间隔计时器似乎能够很好地相互复用,因此我可以在另一个间隔计时器中安装对
    waitpid
    的手动调用,以检查和获取子进程。缺点:子进程等待可能会被人为延迟(我的用例涉及大量频繁的进程创建/销毁),任何
    AnyEvent::child
    监视程序如果安装并成功触发,都会自动捕获子进程,而不会告诉我的interval/waitpid计时器,需要编排,我总觉得我在滥用任何事件

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

因此,根据事件循环的实现情况,可能不会处理其他事件,因为它会一遍又一遍地忙于运行同一个计时器。我不知道您正在使用哪个底层事件循环(检查
$AnyEvent::MODEL
),但如果您查看(纯Perl实现的循环,即MODEL为
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;

更新使用中提供的修复程序可以解决此问题

@steffen ulrich的回答是正确的,但指出了任何事件中一个非常有缺陷的行为:因为没有潜在的事件队列,某些总是报告“就绪”的事件可以无限期地抢占其他事件

以下是一个解决方法:

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

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::delay
的实现安装了一个“NextTick”,那么这里的很多麻烦都可以避免-我不知道这是否相关,但是
AnyEvent
文档中说“这意味着你不能在AnyEvent程序中首先创建一个儿童观察者,yo
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;