読者です 読者をやめる 読者になる 読者になる

はむはむエンジニアぶろぐ

このブログのコンセプトは"ハッキングの為なら愛する家族を傷つけることをいとわない" 自分にとってエンジニアリングは "手段ではなく生きる目的" である

Perl Coroを使ってコルーチンを体系的に学ぶ

f:id:secret_hamuhamu:20150426020452p:plain

Perlには、Coroというコルーチンのライブラリがあります。
Coroを使ってコルーチンを体系的に学んでいこうと思います。

コルーチンとは?

wikipedia:引用

コルーチン(英: co-routine)とはプログラミングの構造の一種。サブルーチンがエントリーからリターンまでを一つの処理単位とするのに対し、コルーチンはいったん処理を中断した後、続きから処理を再開できる。接頭辞 co は協調を意味するが、複数のコルーチンが中断・継続により協調動作を行うことによる。 サブルーチンと異なり、状態管理を意識せずに行えるため、協調的処理、イテレータ、無限リスト、パイプなど、継続状況を持つプログラムが容易に記述できる。


通常、プログラムはメインスレッドと呼ばれるひとつのスレッドで実行され終了します。
しかし、コルーチンではメインスレッドから複数のスレッドを扱う(非同期処理)ことが出来ます。


どうしてそんなことが必要なのかというとI/OとかCPUを使わない遅い処理をメインスレッドでやってしまうと、そこで処理が止まってしまいます。
I/O待ちの間、CPUは余力があります。
その間に、違うお仕事をしてもらおうというものです。
それによりパフォーマンスの向上や全体のタスクが早く終わります。


マルチスレッドとコルーチンは別物みたいです。

  • スレッドを自動で切り替えるのが、マルチスレッド
  • スレッドを任意のタイミングで切り替えるのが、コルーチン


コルーチンやマルチスレッドは、必要性がないのであれば、扱わないほうがいいです。
テストで気づきにくいバグの混入や、プログラムの複雑性が増すから。

逆に必要性があるのであれば、課題を解決してくれる技術であると思います。

Coroについて

PerlCPANで公開されているコルーチンを扱えるライブラリ。
コルーチンだけでなく複数スレッドを扱ううえで便利な機能が揃っている。

協調スレッド方式で、一つのCPUを複数のスレッドで共有をする。
複数のCPUを使って並列に動作するのではなく、一つのCPUで並行に動作する。

後は、使いながら説明する。

サンプルコード

別スレッドの作成とスレッドの切替

以下のコードを記述して実行してみる。

async {
    print "async 1\n";
    cede;   // mainにスレッド実行を権利を譲渡する
    print "async 2\n";
};

print "main 1\n";
cede;   // asyncにスレッド実行を権利を譲渡する
print "main 2\n";
cede;   // asyncにスレッド実行を権利を譲渡する
print "main 3\n";

実行結果

main 1
async 1
main 2
async 2
main 3

async で囲まれたブロックが、別スレッドの作成になる。
async のスレッドは、実行可能状態となり ready queue と呼ばれる実行待ちのキューに格納される。
スレッドを作っただけではダメでメインスレッドから実行権を譲渡してもらわなければならない。


実行権の譲渡 それが、 cede である。
メインスレッドから、 cede を呼ぶことで自身を、 ready queue に格納する。
async側から cede することで、今度はasyncが ready queue に格納される。
そうすることで、メインスレッドが、実行可能状態から実行状態へ移ることが出来る。


asyncに引数を渡して実行

use strict;
use warnings;
use Coro;

my $hoge = "ほげ\n";

async {
    print "@_\n";
    print $hoge;
} 1, 2, 3;

cede;

実行結果

1 2 3
ほげ

async自体は、クロージャと変わらないのでメインスレッドの変数を参照することも出来る。


Coroオブジェクトを使う方法

Coroオブジェクトを生成して、コルーチンを行うことも出来ます。
個人的にはこちらの記法のほうが、分かりやすくて好きです。

use strict;
use warnings;
use Coro;

my $coro = new Coro sub {
      print "async\n";
};

# ready queueに格納
$coro->ready;
$coro->cede;

print "main\n";
async
main

async を扱う方は、asyncで ready queue に入れていましたが、Coroオブジェクトを使う場合は、明示的にready queueに格納してあげる必要があります。


コルーチンから戻り値を受け取る

コルーチンから戻り値を受け取る方法があります。
join を使います。

use strict;
use warnings;
use Coro;

my $coro = async {
      "hello, world\n"
};

my $hello_world = $coro->join;

print $hello_world;
hello, world


Semaphoreを使用した並行スレッド数制御

Semaphore(セマフォ)と呼ばれる仕組みを使用して、並列スレッド数を制御する頃が可能です。

どんな時に使うかというと、大量に並列でhttpリクエストを投げてしまうとwwwサーバが耐えられなくなったりします。
サーバのスペック的に懸念があったりクローラを作る際、誤って人様のサーバを攻撃していたなんてことを防ぐことが出来ます。

並行ではなく、直行であればこんなことは気にすることはないのです。
並行プログラミングのややこしさですね。
ただ、Coroは、とても簡単に 並行スレッド数を制御できます。

use strict;
use warnings;
use Coro;
use Coro::Semaphore;
use Web::Query;
use FurlX::Coro;

my $semaphore = Coro::Semaphore->new(1);

my @coros;
my @urls = qw(
    http://hamuhamu.hatenablog.jp/
    http://www.excite.co.jp/
    http://www.goo.ne.jp/
    http://www.google.co.jp/
    http://www.yahoo.co.jp/
);

for my $url (@urls) {
    push @coros, async {
        my $guard = $semaphore->guard;
        my $ua = FurlX::Coro->new;
        my $res = $ua->get($url);

        print "準備完了\n";
        my $title = Web::Query->new_from_html($res->content)
                ->find('title')->text;

        print "$title\n";
    };
}

$_->join for @coros;

実行結果

準備完了
はむはむエンジニアぶろぐ
準備完了
Excite エキサイト
準備完了
goo
準備完了
Google
準備完了
Yahoo! JAPAN

@urls に指定されたwebサイトのタイトルを取得してくるスクリプト

my $semaphore = Coro::Semaphore->new(1); これが、セマフォです。
newの引数に1を入れてますが、これが同時並行スレッド数です。
1ということは、実質直行と同じです。
1つのスレッドが終了するまで、他のスレッドは実行状態となりません。


実行結果を見ると、ちゃんと順番通りに処理されていることがわかります。

my $guard = $semaphore->guard; guardと呼ばれるメソッドを使用している。
これは何かというと、 $guard 変数がdestroyされた時に、セマフォが持っている実行中のスレッドのカウンタをマイナス1にする。
そうすることで、新たに別のスレッドを実行可能状態から実行状態に移すことが出来る。


今回、cedeとか使っていないけど、 FurlX::Coro を使っているので、自動的にhttpリクエストでスレッドが待ち状態になったら実行権を移すということをやってくれている。

my $semaphore = Coro::Semaphore->new(5); にすると実行結果が変わり、並行で動作していることが分かる。

実行結果

準備完了
Yahoo! JAPAN
準備完了
はむはむエンジニアぶろぐ
準備完了
Google
準備完了
goo
準備完了
Excite エキサイト


Channelで、複数スレッド間のデータ共有

join を使って、戻り値を受け取ることも出来ますが、Channelを使ったほうがやりやすいです。

use strict;
use warnings;
use Coro;
use Coro::Semaphore;
use Web::Query;
use FurlX::Coro;

my $semaphore = Coro::Semaphore->new(5);
my $channel = Coro::Channel->new;

my @coros;
my @urls = qw(
    http://hamuhamu.hatenablog.jp/
    http://www.excite.co.jp/
    http://www.goo.ne.jp/
    http://www.google.co.jp/
    http://www.yahoo.co.jp/
);

for my $url (@urls) {
    push @coros, async {

        my $guard = $semaphore->guard;
        my $ua = FurlX::Coro->new;
        my $res = $ua->get($url);

        $channel->put($url);

        my $title = Web::Query->new_from_html($res->content)
                ->find('title')->text;

        $channel->put($title);
        $channel->shutdown;
    };
}

$_->join for @coros;

while( my $got = $channel->get() ){
    print $got, "\n";
}

実行結果

http://www.yahoo.co.jp/
Yahoo! JAPAN
http://hamuhamu.hatenablog.jp/
はむはむエンジニアぶろぐ
http://www.google.co.jp/
Google
http://www.goo.ne.jp/
goo
http://www.excite.co.jp/
Excite エキサイト

$channel->put() で、データを詰めて $channel->get() で受け取ることが出来ます。

まとめ

中途半端であるが、基本的な使い方でした。

こちらのサイトが参考になった。
http://d.hatena.ne.jp/starsky5/20091009/1255063498

最近読んだ本

初めてのPerl 第6版

初めてのPerl 第6版

Effective Perl 第2版

Effective Perl 第2版

Perl徹底攻略 (WEB+DB PRESS plus)

Perl徹底攻略 (WEB+DB PRESS plus)

Perlベストプラクティス

Perlベストプラクティス