以前Coroで効率よくスクレイピングなどと書いたんですが、恥ずかしながら書いた時はCoroがコルーチンを意味しているとは知らず、マルチスレッドを扱うライブラリだと認識していました。もう少し理解したいという事で、以下の3つの方法で並列にHTTPリクエストを発行して、その比較を行ってみました。
- Coroによるマルチスレッド処理
- threadsによるマルチスレッド処理
- ForkManagerによるマルチプロセス処理
テストしてみたのは以下のコードです。Coroだけ別なのはCoro::LWPをインポートすると、LWPが全部Coro仕様に上書きされる為です。処理は、はてなブックマークのホットエントリのリストを取得して、リストのそれぞれのページへアクセスしてタイトルを取得するというものです。
#!/usr/bin/perl
use strict;
use warnings;
use Perl6::Say;
use YAML;
use Time::HiRes;
use threads();
use LWP::Simple;
use Web::Scraper;
use Parallel::ForkManager;
use IPC::Shareable;
use Benchmark qw(timethese cmpthese);
my @links = test_urls(); # ホットエントリのURLリストを取得して、
my $result = timethese(10, { # それぞれの方法で各URLのタイトルを取得するのを計測する
'single' => sub { do_sthread(\@links); },
'multi_thread' => sub { do_mthread(\@links); },
'fork_manager' => sub { do_mprocess(\@links); }
});
cmpthese($result);
exit;
# はてなブックマークから現在のホットエントリーのURLリストを取得
sub test_urls {
my $source = "http://b.hatena.ne.jp/hotentry";
my $scraper = scraper {
process '//div[@class="entry-body"]/h3/a', 'links[]' => '@href';
};
return @{$scraper->scrape(get($source))->{'links'}};
}
# 指定したURLにあるページのタイトルを取得する
sub get_title {
my $url = shift;
my $scraper = scraper {
process '//title', 'title' => 'TEXT';
};
return $scraper->scrape(get($url))->{'title'};
}
# シングルスレッドでリクエストを発行
sub do_sthread {
my $links = shift;
my $t1 = Time::HiRes::time;
my @result;
foreach my $link (@$links) {
push(@result, get_title($link));
}
say "Single Thread Time took: ", Time::HiRes::time - $t1;
return @result;
}
# マルチスレッドでリクエストを発行
sub do_mthread {
my $links = shift;
my $t1 = Time::HiRes::time;
my @threads = map {
threads->create(\&get_title, $_);
} @$links;
my @result;
push (@result, $_->join) for @threads;
say "Multi Thread Time took: ", Time::HiRes::time - $t1;
return @result;
}
# Paralell::ForkManagerで並列処理
sub do_mprocess {
my $links = shift;
my $t1 = Time::HiRes::time;
my $pm = Parallel::ForkManager->new(10);
my $handle = tie my @result, 'IPC::Shareable', undef, { destroy => 1 };
@result = ();
for my $link (@$links) {
$pm->start and next;
my $title = get_title($link);
$handle->shlock;
push(@result, $title);
$handle->shunlock;
$pm->finish;
}
$pm->wait_all_children;
say "Multi Process Time took: ", Time::HiRes::time - $t1;
return @result;
}
以下はCoroのテスト。一部省略。
use Coro;
use Coro::LWP;
sub do_coro {
my $links = shift;
my $t1 = Time::HiRes::time;
my (@coro, @result) = (), ();
for my $link (@$links) {
push @coro, async {
push @result, get_title($link, 1);
};
}
$_->join for @coro;
say "Coro Time took: ", Time::HiRes::time - $t1;
return @result;
}
で、結論をはしょって書くと以下のようになりました。singleというのはリクエストを並列化せずに行ったものです。
fork_manager: 313 wallclock secs ( 0.11 usr 0.59 sys + 159.14 cusr 30.70 csys = 190.54 CPU) @ 14.29/s (n=10)
multi_thread: 250 wallclock secs (179.52 usr + 10.47 sys = 189.99 CPU) @ 0.05/s (n=10)
coro: 220 wallclock secs (118.99 usr + 3.32 sys = 122.31 CPU) @ 0.08/s (n=10)
single: 714 wallclock secs (127.31 usr + 4.17 sys = 131.48 CPU) @ 0.08/s (n=10)
予想に反して、coroがwallclockを見てもCPU負荷を見ても性能が良いと出ています。(個人的にはthreadsが一番早いと思っていました。)厳密に測定してないので、あくまで参考程度ですが。
以上で、結論としてはソースコードも短くて簡潔ですし、コルーチン的な機能は何も使ってないけど、Coroをクローラ用途で使ってもいいじゃない!ってところです。あとテストコードはこれです。
prada コレクション