Perl:非阻塞管道-仅收到一条消息

时间:2018-10-30 20:51:41

标签: perl

几周前,我问了一个有关实现非阻塞的一对多子管道的问题,@ mob here

回答了这一问题。

但是,我注意到,如果孩子在退出之前发布了多条消息,则父母只有在稍晚才阅读时才会得到第一条消息。

示例代码:

use IO::Handle;
use POSIX ':sys_wait_h';
pipe(READER,WRITER);
WRITER->autoflush(1);

sub child_process {
    close READER;  # also a best but optional practice
    srand($$);
    my $id = 0;
        sleep 1 + 5*rand();
        $id++;
        print "Child Pid $$ sending message $id now...\n";
        print WRITER "$id:Child Pid $$ is sending this - Message 1\n";
        print WRITER "$id:Child Pid $$ is sending this - Message 2\n";
        exit 0;
}

if (fork() == 0) {
    child_process();
}

# parent
my ($rin,$rout) = ('');
vec($rin,fileno(READER),1) = 1;
while (1) {
     # non-blocking read on pipe
     my $read_avail = select($rout=$rin, undef, undef, 0.0);
     if ($read_avail < 0) {
         if (!$!{EINTR}) {
             warn "READ ERROR: $read_avail $!\n";
             last;
         }
     } elsif ($read_avail > 0) {
         chomp(my $line = <READER>);
         print "Parent Got $$: '$line'\n";
     } else {
            print STDERR "No input ... do other stuff\n";
     }
     sleep 5;
}
close WRITER;  # now it is safe to do this ...

预期输出:

我应该同时收到两条消息。

我得到的是:只有第一条消息

No input ... do other stuff
No input ... do other stuff
Child Pid 8594 sending message 1 now...
Parent Got 8593: '1:Child Pid 8594 is sending this - Message 1'
No input ... do other stuff

这应该是非阻塞读取,所以回家了,它不能在下一次迭代中获取数据吗?是因为孩子退出了吗?我尝试在父级中执行while (chomp(my $line = <READER>)),但这样做会阻止我执行该操作。

3 个答案:

答案 0 :(得分:3)

好像您正在混合缓冲的I / O和未缓冲的I / O。 <READER>(和readline(READER))是缓冲的输入操作。第一次在文件句柄上调用readline时,Perl会尝试从该句柄读取多达8K的数据,并将其中的大部分保存在内存缓冲区中。下次您在同一文件句柄上调用readline时,Perl将尝试返回缓冲区中的数据,然后再尝试从文件中读取更多数据。这是为了提高效率。

select是用于无缓冲I / O的操作。它告诉您输入是否在文件句柄本身上等待,但看不到数据是否在缓冲区中等待。

一个笨拙的选择是使用sysreadgetc从管道中提取数据。这很不方便,因为您必须自己将输入分成几行。

... if ($read_avail > 0) {
    my $n = sysread READER, my $lines, 16384;
    chomp($lines);
    my @lines = split /\n/, $lines;
    print "Parent Got $$: '$_'\n" for @lines;
} ...

可能的工作是在列表上下文中从文件句柄读取。

chomp(my @lines = <READER>);
seek READER, 0, 1;

应该从缓冲区和文件句柄中读取所有可用数据,并且理论上它将使您的缓冲区为空,因此下一个<READER>调用将类似于未缓冲的读取。 (seek语句清除了文件句柄上的EOF条件,以便稍后在有更多输入到达时可以从文件句柄中读取)。

(ETA:不,这是行不通的。它将在READER上阻塞,直到孩子关闭管道的末端为止)


select的文档有此警告

  

警告:请勿尝试混合缓冲的I / O(例如     “ read”或“ <FH>”加上“ select”,除非POSIX允许,     即使在POSIX系统上也是如此。您必须使用“ sysread”     代替。

答案 1 :(得分:2)

您每次迭代最多只能读取一行,而不是读取管道上的所有可用数据。之后,也许select()不再表明它可读取。请注意,由于要派生,您还需要在waitpid退出子进程后获得它(在阻塞模式下,waitpid将等待其退出),这将返回子进程的退出代码。

我建议使用事件循环来管理进程之间的管道,因为它及其帮助程序模块将管理分支进程和交换数据的所有奇怪细节。这就是使用IO::Async的情况。

use strict;
use warnings;
use IO::Async::Loop;
use IO::Async::Channel;
use IO::Async::Routine;

my $channel = IO::Async::Channel->new;

sub child_process {
    my $id = 0;
    sleep 1 + 5*rand();
    $id++;
    print "Child Pid $$ sending message $id now...\n";
    $channel->send(\"$id:Child Pid $$ is sending this - Message 1\n");
    $channel->send(\"$id:Child Pid $$ is sending this - Message 2\n");
}

my $loop = IO::Async::Loop->new;
my $f = $loop->new_future;
my $routine = IO::Async::Routine->new(
  channels_out => [$channel],
  code => \&child_process,
  on_return => sub { my $routine = shift; $f->done(@_) },
  on_die => sub { my $routine = shift; $f->fail(@_) },
);
$loop->add($routine);

$channel->configure(on_recv => sub {
  my ($channel, $ref) = @_;
  print "Parent Got: '$$ref'\n";
});

# wait for Future to complete (process finishes) or fail (process fails to start or dies)
my $exitcode = $f->get;
print "Child exited with exit code $exitcode\n";

请注意,IO::Async::Channel只是围绕IO::Async::Stream的抽象,用于在进程之间发送数据结构,而IO::Async::Routine是围绕IO::Async::Process的抽象(或者在Windows系统上是线程) ),以将频道设置为派生代码。 IO::Async::Function另外是IO :: Async :: Routine的高层包装,它可以管理fork / thread池以不同的输入多次运行子例程,并在父级中接收返回值。因此,您可以根据需要潜水的深度有很多水平。

答案 2 :(得分:1)

好吧,我似乎看到了@Grinnz提出的第一个建议,即使用定义良好的框架的好处。我以为我需要三轮车,但看起来我正在慢慢地用螺栓和螺母建造一辆宝马车。

@mob和@grinnz的建议是正确的。这是缓冲区/ VS /非缓冲区的情况。

chomp(my @lines = <READER>);
seek READER, 0, 1;

不起作用。它会锁定。

此食谱食谱有效,但明天我将对其进行调整/测试(source)。到目前为止一切顺利:

use IO::Handle;
use POSIX ':sys_wait_h';
use Symbol qw(qualify_to_ref);
use IO::Select;
pipe(READER,WRITER);
WRITER->autoflush(1);

sub sysreadline(*;$) {
    my($handle, $timeout) = @_;
    $handle = qualify_to_ref($handle, caller( ));
    my $infinitely_patient = (@_ == 1 || $timeout < 0);
    my $start_time = time( );
    my $selector = IO::Select->new( );
    $selector->add($handle);
    my $line = "";
SLEEP:
    until (at_eol($line)) {
        unless ($infinitely_patient) {
            return $line if time( ) > ($start_time + $timeout);
        }
        # sleep only 1 second before checking again
        next SLEEP unless $selector->can_read(1.0);
INPUT_READY:
        while ($selector->can_read(0.0)) {
            my $was_blocking = $handle->blocking(0);
CHAR:       while (sysread($handle, my $nextbyte, 1)) {
                $line .= $nextbyte;
                last CHAR if $nextbyte eq "\n";
            }
            $handle->blocking($was_blocking);
            # if incomplete line, keep trying
            next SLEEP unless at_eol($line);
            last INPUT_READY;
        }
    }
    return $line;
}
sub at_eol($) { $_[0] =~ /\n\z/ }

sub child_process {
    close READER;  # also a best but optional practice
    srand($$);
    my $id = 0;
        sleep 1 + 5*rand();
        $id++;
        print "Child Pid $$ sending message $id now...\n";
        print WRITER "$id:Child Pid $$ is sending this - Message 1\n";
        print WRITER "$id:Child Pid $$ is sending this - Message 2\n";
        exit 0;
}

if (fork() == 0) {
    child_process();
}

# parent
my ($rin,$rout) = ('');
vec($rin,fileno(READER),1) = 1;
while (1) {
     # non-blocking read on pipe
     while ((my $read_avail = select($rout=$rin, undef, undef, 0.0)) !=0) 
     {
        if ($read_avail < 0) {
                 if (!$!{EINTR}) {
                 warn "READ ERROR: $read_avail $!\n";
                last;
                }
        }
        elsif ($read_avail > 0) {
         chomp(my $line = sysreadline(READER));
         print "Parent Got $$: '$line'\n";
         print "END MESSAGE\n";
       }
     }
     print STDERR "input queue empty...\n";
     print "Sleeping main for 5...\n";
     sleep 5;
}
close WRITER;  # now it is safe to do this ...