Pixel Pedals of Tomakomai

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

recursive blocking wait detected への対処

連休の非同期祭も(疲れたので)これでラストです。

AnyEvent(5.2)でこんなコード書くと、「recursive blocking wait detected」が出ます。

use strict;
use warnings;
use AnyEvent;

sub main {
	foreach my $i (1 .. 5) {
		my $cv = AE::cv;
		my $t = AE::timer $i, 0, sub {
			$cv->send($i);
		};

		print $cv->recv, "\n";
	}
}

my $t = AE::timer  0, 0, \&main;
AE::cv->recv;
# 結果
# EV: error in callback (ignoring): AnyEvent::CondVar: recursive blocking wait detected at XXXX.pl line 12

先にまとめ

condvarのrecvは、平たく言えばメインループ(イベントループ)です。メインループの中でさらにメインループを作ることはできません。

ただし、recvはループを回さないこともあります(対処法1と2を参照)。メインループを回すrecvの中で、ループを回さないrecvを使うことは可能です。

対処法1: cbを使う

(発表見てないですがボケてた、思いっきり聞いてましたw)YAPCでmiyagawaさんが紹介していた方法です。podによれば、

You can ensure that -recv never blocks by setting a callback and only calling ->recv from within that callback (or at a later time). This will work even when the event loop does not support blocking waits otherwise.

AnyEvent

です。言われた通りにcondvar->cb にコールバックを渡し、その中で recv を呼べばなんとエラーになりません。

use strict;
use warnings;
use AnyEvent;

sub main {
	foreach my $i (1 .. 5) {
		my $cv = AE::cv;
		my $t; $t = AE::timer $i, 0, sub {
			$cv->send($i);
			undef $t;
		};
		$cv->cb( sub {
			print $cv->recv, "\n";
		} );
	}
}

my $t = AE::timer  0, 0, \&main;
AE::cv->recv;
からくり

recvは、「値がsendされてくるまでイベントループを回す」という実装になっています。逆に言えば、sendが終わった後はイベントループを回さないので、イベントループの中で使うことができます。

condvarのcbはsendされたタイミングで呼ばれるので、イベントループを回しません。

対処法2: Coroを使う

podによれば、

This condition can be slightly loosened by using Coro::AnyEvent, which allows you to do a blocking ->recv from any thread that doesn't run the event loop itself.

AnyEvent

だそうですので、アドバイスに従ってasyncしてみると動きます。

use strict;
use warnings;
use AnyEvent;
use Coro;
use Coro::AnyEvent;

sub main {
	foreach my $i (1 .. 5) {
		my $cv = AE::cv;
		my $t; $t = AE::timer $i, 0, sub {
			$cv->send($i);
			undef $t;
		};
		async {
			print $cv->recv, "\n";
		};
	}
}

my $t = AE::timer  0, 0, \&main;
AE::cv->recv;
からくり

Coro::AnyEventの環境下では、recvはscheduleであって、メインループを進めません*1。また、メインループが1つ進むと、scheduleが走ります。

まずメインループのスレッドでmainが呼ばれ、ここでasyncでスレッドが5つ上がります。main関数を抜けるとscheduleが走り、5つのスレッドに処理が回ってきます。各スレッドの中ではrecvによって再びscheduleが走り、sendが呼ばれるまでメインループのスレッド(idleスレッド)へ制御が移り、メインループが進められます。

注意

本来 recursive blocking wait detected が出るようなコードに use Coro::AnyEvent をつけると、このエラーが出なくなり、原因の調査が困難になります*2

事故を防ぐため、メインループを実行しているスレッドでは、cbの外ではrecvを呼ばないように気をつけましょう。

*1:scheduleの結果idleスレッドに制御が移ると、こいつがメインループを進めます。

*2:Coro::AnyEvent の環境では、recvはメインループではないからでしょう。