CoroとthreadsとForkManagerでウェブページ取得の比較をしてみた

以前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をクローラ用途で使ってもいいじゃない!ってところです。あとテストコードはこれです。

2 thoughts on “CoroとthreadsとForkManagerでウェブページ取得の比較をしてみた”

Leave a Reply

Your email address will not be published. Required fields are marked *