并行读取STDIN时$ _ empty

时间:2017-01-29 20:28:26

标签: perl while-loop parallel-processing

我有一个遗留项目,它从STDIN中获取大量数据并在perl脚本中逐行处理。行顺序并不重要。 这需要很长时间,所以我想并行完成。

经过一些研究后,我发现Parallel::Loops似乎合适但我无法使其正常工作,因为$_为空。我的代码是:

#Initialize all vars etc

$pl->while ( sub { <STDIN> }, sub {
    print $_       # but $_ is empty
}

从STDIN ir parallel读取的其他方式也很受欢迎。

<小时/> 的更新

在我收到所有帮助后,我可以管理一些工作代码,谢谢。我将做一个简短的摘要。澄清:

  1. 这是一种解析器,它有超过3000行的正则表达式和 自动生成的条件。

  2. 我用于测试的输入是带有POS标记的文本,此文件中有1071406行。

  3. 我的硬件是:SSD光盘,中端i5上一代和8GB RAM DDR4。

  4. 结论:

    1. 由于评论建议IO操作使我的脚本变慢。
    2. 所有建议都带来了改进,特别是包括处理一堆线而不是逐行处理的建议。
    3. 答案包含非常有用的线程实现,以供将来工作。
    4. Framework Parallel :: ForkManager在执行时间中引入了很多延迟。我总是在5分钟后杀死脚本,因为没有并行性的脚本大约需要6个。
    5. Framework Parallel :: Loops引入了一些改进。该脚本大约需要3分钟才能完成。
    6. 使用GNU parallel是一种简单的优化方法。
    7. 使用软件包Threads我得到了最好的时间,1min45secs,但它非常接近GNU parallel,所以你试试看,并努力移植代码。
    8. 使用线程包,如@ikegami答案读取一堆行,时间与应用@tanktalus解决方案的次数相同,后者逐行读取。
    9. 最后,我要使用@ikegami解决方案,我认为当数据量增加时会更好。我将要处理的行数调整为100.000,因为它的结果比10.000更好。这个差异是8秒的aprox。

      下一个自然步骤是将所有内容写入文件而不是使用STDOUT,我希望这有助于缩短时间。

3 个答案:

答案 0 :(得分:4)

$_永远不会设置,因为您从未分配给$_

不要忘记

while (<STDIN>) { ... }

的缩写
while (defined( $_ = <STDIN> )) { ... }

这意味着您希望使用以下内容:

$pl->while ( sub { defined( $_ = <STDIN> ) }, sub {
    print $_;
}

那就是说,打击$_是一个坏主意。它很可能被调用者中的for (...)别名化为其他变量。

这意味着你应该使用以下内容:

my $line;
$pl->while ( sub { defined( $line = <STDIN> ) }, sub {
    print $line;
}

您可能会发现将工作分解为更粗糙的单位,这些线条会产生更好的性能,因为它会减少无意中听到的工作比率。

use constant WORK_UNIT_SIZE => 100;

my $done = 0;
my @lines;
$pl->while ( sub {
    @lines = ();
    return 0 if $done;

    while (@lines < WORK_UNIT_SIZE) {
        my $line = <>;
        if (!defined($line)) {
            $done = 1;
            return 0+@lines;
        }

        push @lines, $line;
    }

    return 1;
}, sub {
    for (@lines) {
        print $_;
    }
}

最后,您应该重复使用它们,而不是为每个工作单元创建新任务!以下使用线程演示了这一点。

use threads            qw( async );
use Thread::Queue 3.01 qw( );

use constant NUM_WORKERS    => 8;
use constant WORK_UNIT_SIZE => 100;

sub worker {
    my ($job) = @_;
    for (@$job) {
        print $_;
    }
}

my $q = Thread::Queue->new();
$q->limit(NUM_WORKERS * 4);

async { while (defined( my $job = $q->dequeue() )) { worker($job); } }
    for 1..NUM_WORKERS;

my $done = 0;    
while (!$done) {
    my @lines;
    while (@lines < WORK_UNIT_SIZE) {
        my $line = <>;
        if (!defined($line)) {
            $done = 1;
            last;
        }

        push @lines, $line;
    }

    $q->enqueue(\@lines) if @lines;
}

$q->end();
$_->join for threads->list;

答案 1 :(得分:1)

我不知道使用Parallel::Loops(可能有)的具体好处。 Parallel::ForkManagerParallel::Loops使用的内容相同。

use warnings;
use strict;
use feature 'say';

use Parallel::ForkManager;   

my $max_procs = 30; 
my $pm = Parallel::ForkManager->new($max_procs);   

# Retrieve data returned by children in the callback
my %ret_data;      
$pm->run_on_finish( sub { 
    my ($pid, $exit, $ident, $signal, $core, $dataref) = @_; 
    $ret_data{$pid} = $dataref;
});

while (my $input = <STDIN>)
{
    chomp($input);

    $pm->start and next;
    my $ret = run_job($input);
    $pm->finish(0, \$ret);
}
$pm->wait_all_children;

foreach my $pid (keys %ret_data) {
    say "$pid returned: ${$ret_data{$pid}}";
}

sub run_job { 
    my ($input) = @_; 
    # your processing
    return $input;    # to have something to check
} 

此代码返回子进程的标量,即单个值。您可以返回任何数据结构,请参阅文档中的 Retrieving data structures from child processesthis post作为示例。

数据通过文件返回,这可能会减慢大数据或许多快速流程的速度。

如果在终端上进行测试,请使用Ctrl-d进行测试(或在last if $input !~ /\S/;后添加chomp以使用空行停止 - 但不要将数据传递给STDIN其他方式)。

明确指出,每个STDIN读取只需要处理一行。然后我们应该在产生一个新进程之前收集更多的行,否则会有太多的开销。

my $num_lines_to_collect = 1000;

my @lines_to_process;         # collect lines for each fork

while (my $input = <STDIN>)
{
    chomp($input);
    push @lines_to_process, $input;
    next if $. % $num_lines_to_collect != 0;

    $pm->start and next;
    my $ret = run_job( \@lines_to_process );
    $pm->finish(0, \$ret);

    @lines_to_process = ();   # empty it for the next round
}
$pm->wait_all_children;

我们向数组@lines_to_process添加行,并且仅当当前行号$.$num_lines_to_collect的倍数时才继续使用新的分叉。因此,每个$num_lines_collect都会启动一项工作,因此每项工作都会处理得那么多。我将其设置为1000,即实验。

答案 2 :(得分:0)

这里最简单的方法可能是创建一个线程池,每个线程监听同一个队列,然后让一个线程(可能是主线程)读取文件并将每一行推送到队列。

use strict;
use warnings;
use Thread qw(async);
use Thread::Queue;

my $q = Thread::Queue->new();
$q->limit(32); # no point in reading in more than this into memory.

my @thr = map {
    async {
        while (defined (my $line = $q->dequeue()) ) {
            print $line;
        }
    };
} 1..4; # 4 worker threads

while (<STDIN>)
{
    $q->enqueue($_);
}
$q->end();

$_->join for Thread->list;

正如警告一样,请注意是否需要将数据从工作线程推送回主线程。它并不像其他语言那样微不足道。

更新:从线程切换到线程。虽然异步函数被记录为返回线程对象,但这对我来说似乎不起作用,因此也必须更改连接。