Pixel Pedals of Tomakomai

北海道苫小牧市出身の初老の日常

Coroを使って、コールバック形式を普通の形式に書き換えてみた

非同期ブーム第二段です。

書き換え方

func_by_cb( $cb, @params ) みたいな形式があった時に、Coroのrouse_cbを使うと、通常の関数呼び出しの形式で呼び出せる関数に変換ができます。こんな感じで。

sub func_by_coro {
    func_by_cb( Coro::rouse_cb, @_ );
    return Coro::rouse_wait;
}

変換前の関数(func_by_cb)は、以下のように使います。

func_by_cb( sub {
    print @_, "\n";
}, 1, 2, 3);

対して、変換後の関数(func_by_coro)では、この処理を以下のように書けます。

print func_by_coro(1,2,3), "\n";

使い勝手は一目瞭然ですね!

コールバックがAnyEventで実現されている場合

コールバックがAnyEventのメインループを使って実現されている場合(ほとんどの場合がこうでしょう)は、もう一捻りが必要です。なぜかと言うと、この書き換えでは rouse_wait で別スレッドに処理を譲ってコールバックを待とうとするのですが、この段階ではreadyなスレッドがなくて「deadlock detected」となるからです。コールバックを呼んでもらうには、rouse_wait後にAnyEventのメインループを進めてもらう必要があります。

そこで、「use Coro::AnyEvent;」をしてやります。こうすると、 $Coro::idle が書き換えられ、readyなスレッドがない時にdeadlockする代わりに、メインループを回しつつreadyなスレッドを探してくれるようになります。

いやあ、よくできてますね。

余談: rouse_cbとrouse_waitの原理

rouse_cbとrouse_waitは、自分で書くことも出来ます。先ほどのfunc_by_coroを、rouse_cbとrouse_waitを使わずに書くと大体こんな感じです*1

sub func_by_coro{
    my @params = @_;

    my $current = $Coro::current;
    my $done = 0;
    my @ret;

    func_by_cb( sub {
        @ret = @_;
        $done = 1;
        $current->ready;
    }, @params );

    schedule while !$done;

    return @ret;
}

【9/24追記】podを見習って、$doneフラグをつけた。CoroはCoro::State::listで露出されてるので、他スレッドから簡単にreadyできる。よってこの方がいいと思われる。

さらに余談: AnyEventだけでも書き換えられる気がする

ここまで書いておいてなんですが、CoroじゃなくてもAnyEventで同じような書き換えができることに気がつきました。

sub func_by_coro {
    my $cv = AE::cv;
    func_by_cb( sub {
        $cv->send(@_);
    }, @_ );
    return $cv->recv;
}

となると、この書き換えに関してはCoroを使う利点はないかもしれません。

【9/22 追記】この書き方をすると、コールバックから関数を呼んだときに「recursive blocking wait detected」となります。podにも「This condition can be slightly loosened by using Coro::AnyEvent」とありますので、素直にCoroの力を借りた方が良さそうです。

*1:wantarrayとか端折ってるので注意。