并行叉管理器,DBI。比分叉前快,但仍然太慢

Parallel-ForkManager, DBI. Faster than before forking, but still too slow

我有一个非常简单的更新数据库的任务。

my $pm = new Parallel::ForkManager(15);
for my $line (@lines){
    my $pid = $pm->start and next;
    my $dbh2 = $dbh->clone();
    my $sth2 = $dbh2->prepare("update db1 set field1=? where field2 =?");           
    my ($field1, $field2) = very_slow_subroutine();
    $sth2->execute($field1,$field2);
    $pm->finish;        
} 
 $pm->wait_all_children;    

我可以只使用 $dbh2->do,但我怀疑这是一个缓慢的原因。

有趣的是,这 15 个进程(或我指定的任何进程)的启动速度似乎非常快,但之后速度急剧下降,仍然明显比没有分叉时快,但我希望更多...

编辑:

very_slow_subroutine 是从 Web 服务获取答案的子项。该服务可以在几分之一秒到几秒内响应超时。我必须问一万遍……我想做叉子的原因。

如果这很重要——我在 Linux。

并行性是否有用取决于你的瓶颈在哪里。如果您的 CPU 具有 4 个内核是瓶颈,则分叉 4 个进程可能会导致事情在最好情况下的大约 1/4 内完成,但产生 15 个进程不会改善更多。

如果您的瓶颈更有可能出现在 I/O 中,则启动 15 个竞争相同 I/O 的进程不会有太大帮助,尽管在您有大量内存的情况下用作文件缓存,some improvement 可能是可能的。

要探索系统的限制,请考虑以下程序:

#!/usr/bin/env perl

use strict;
use warnings;

use Parallel::ForkManager;

run(@ARGV);

sub run {
    my $count = @_ ? $_[0] : 2;
    my $pm = Parallel::ForkManager->new($count);
    for (1 .. 20) {
        $pm->start and next;
        sleep 1;
        $pm->finish;
    }
    $pm->wait_all_children;
}

我的老式笔记本电脑有一个 CPU 2 核。让我们看看我得到了什么:

TimeThis :  Command Line :  perl sleeper.pl 1
TimeThis :  Elapsed Time :  00:00:20.735

TimeThis :  Command Line :  perl sleeper.pl 2
TimeThis :  Elapsed Time :  00:00:06.578

TimeThis :  Command Line :  perl sleeper.pl 4
TimeThis :  Elapsed Time :  00:00:04.578

TimeThis :  Command Line :  perl sleeper.pl 8
TimeThis :  Elapsed Time :  00:00:03.546

TimeThis :  Command Line :  perl sleeper.pl 16
TimeThis :  Elapsed Time :  00:00:02.562

TimeThis :  Command Line :  perl sleeper.pl 20
TimeThis :  Elapsed Time :  00:00:02.563

所以,运行 最多 20 个进程让我总共 运行 超过 2.5 秒的时间来休眠 1 秒 20 次。

另一方面,只有一个进程,睡眠一秒 20 次只需要 20 多秒。这是一个巨大的改进,但它也表明当您有 20 个进程每个休眠一秒钟时,管理开销超过 150%。

这是并行编程的本质。关于您可以期待的内容,有很多正式的治疗方法,但是 Amdahl's Law 需要阅读。

Parallel::ForkManager 不会神奇地使事情变得更快,它只是让您 运行 您的代码同时执行多次。为了从中获益,您必须针对并行性设计代码。

这样想。从到店、购物、装车、回来、卸车,你需要10分钟。你需要得到 5 负载。你一个人可以在 50 分钟内完成。那是串行工作。 10分钟* 5趟一趟= 50分钟。

假设您有四个朋友帮忙。你们同时出发去商店。还有 5 次行程,它们仍然需要 10 分钟,但是因为您是并行进行的,所以总时间只有 10 分钟。

但无论您要进行多少次旅行或需要帮助多少朋友,都不会少于 10 分钟。这就是为什么这个过程启动得很快,每个人都进入他们的汽车并开车去商店,但是有一段时间没有任何反应,因为每个人仍然需要 10 分钟来完成他们的工作。

这里也是一样。您的循环体需要 X 时间才能 运行。如果你迭代它 Y 次,它将花费 X * Y 现实世界的人类时间 运行。如果你 运行 它并行 Y 次,理想情况下只需要 X 时间就可以 运行。每个并行 worker 仍然必须花费 X 时间执行整个循环体。

为了进一步加快速度,您必须打破 very_slow_subroutine 的大瓶颈并使 that 并行工作。您的 SQL 非常简单,您应该将精力集中在优化和并行性上。

假设商店真的很近,开车只需 1 分钟(这是您的 SQL 更新),但购物、装货和卸货需要 9 分钟(这是 very_slow_subroutine)。如果你有 5 辆车和 15 个朋友会怎样?每辆车载 3 人。开车往返商店需要同样的时间,但现在三个人一起购物,装卸只需4分钟。现在每次旅行只需 5 分钟,而不是 10 分钟。

这表示重新设计 very_slow_subroutine 以并行执行其工作。如果它只是一个大循环,你可以在那个循环上放更多的工人。如果是一系列缓慢的操作,你将不得不重新设计它以利用并行执行。

如果你使用太多的工人,你可能会堵塞系统,这取决于瓶颈是什么。如果它受 CPU 约束并且你有 2 个 CPU 核心,你可能会看到性能提升多达 3 到 5 个工人((cores * 2)+1 是一个很好的经验法则)并且在那之后性能会由于 CPU 在进程之间切换的时间比工作多,所以下降了。如果瓶颈是 IO 或外部服务(通常是数据库和网络调用的情况),您会看到很多工作人员处理该问题的效率很高。当一个进程正在等待磁盘或网络操作时,其他进程可能正在使用您的 CPU.