以前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をクローラ用途で使ってもいいじゃない!ってところです。あとテストコードはこれです。
関連する記事
タグ: coro, forkmanager, perl, threads

