だいぶ習熟したつもりでも、いまだにweakenにはビビらされるのでメモ。
検証環境
- Xubuntu 12.04 32bit
- perl v5.14.2 (Ubuntuレポジトリに入ってるやつ)
- AnyEvent 7.05
- EV 4.15
概要
オブジェクトにコールバック関数を登録する場合、循環参照を防ぐためにコールバック関数にクロージャとして取り込む変数をweakenするのはよくあることだが、その場合、 コールバック関数の実行内容によって weak変数の指すオブジェクトがDESTROYされる可能性があることに注意しないといけない。
例
このあいだ、こんな感じのモジュールを書いた。
package Handle::Wrapper;
use strict;
use warnings;
use AnyEvent::Handle;
use Scalar::Util qw(weaken);
sub new {
my ($class, $fh) = @_;
my $self = bless {
handle => AnyEvent::Handle->new(fh => $fh),
next_read_callbacks => []
};
$self->_init_callbacks();
return $self;
}
sub _init_callbacks {
my ($self) = @_;
weaken $self; ######################################### (1)
$self->{handle}->on_read(sub {
my ($handle) = @_;
my $data = $handle->{rbuf};
## my $strong_self = $self; ####################### (2)
$handle->{rbuf} = "";
for my $cb (@{$self->{next_read_callbacks}}) { #### (3)
$cb->($data)
}
$self->_clear_next_read(); ######################## (4)
});
}
sub _clear_next_read {
my ($self) = @_;
@{$self->{next_read_callbacks}} = ();
}
sub register_next_read {
my ($self, $callback) = @_;
push(@{$self->{next_read_callbacks}}, $callback);
}
sub DESTROY {
warn "Handle destroyed.\n";
}
1;
要はAnyEvent::Handleのラッパーであり、next_read_callbacksに格納したコールバック関数を、ファイルハンドルから読み出したデータに対して1回だけ起動する、というもの。
この機能を実現するために、_init_callbacksメソッドでAnyEvent::Handleのon_readメソッドを読んでコールバック関数を登録しているが、その前にweaken $self;を実行している(1)。これはon_readコールバックの中で$selfを使っているため(3,4)である。weakenをしないとコールバック関数というクロージャを介した循環参照が発生し、$selfがメモリから解放されなくなる。
このように循環参照によるメモリリークを防ぐためにweakenは重宝するわけだが、逆に循環参照を駆使してオブジェクトの寿命を延ばすテクニックもある。
例えば、上記のモジュールは以下のようにして使う。
package main;
use strict;
use warnings;
use EV;
use AnyEvent;
use AnyEvent::Socket qw(tcp_server);
use AnyEvent::Handle;
use Handle::Wrapper;
my $cv = AnyEvent->condvar;
tcp_server "127.0.0.1", 18888, sub {
my $fh = shift;
my $handle = Handle::Wrapper->new($fh);
$handle->register_next_read(sub { ########## (5)
my $data = shift;
warn "Received $data\n";
undef $handle; ######################### (6)
warn "undef handle\n";
$cv->send;
});
};
my $client_handle = AnyEvent::Handle->new(
connect => ["127.0.0.1", 18888],
on_connect => sub {
my ($handle) = @_;
$handle->push_write("foobar");
},
);
$cv->recv;
TCPコネクションを受け付けるとHandle::Wrapperオブジェクトを作成し、コールバック関数を登録する(5)。この時、コールバック関数内にundef $handle;と書くことで、意図的に循環参照を作る(6)。$handle変数のスコープはtcp_serverのコールバック関数内限定だが、循環参照があるため、$handleオブジェクトはメッセージ受信まで存続する。AnyEventではこういった意図的な循環参照がしばしば用いられる。
さて、上記スクリプトを実行すると以下のような結果になる。
$ perl wrapper_user.pl
Received foobar
Handle destroyed.
undef handle
EV: error in callback (ignoring): Can't call method "_clear_next_read" on an undefined value at wrapper_user.pl line 28.
これは、undef $handle;の実行時点(6)で$handleオブジェクトがDESTROYされてしまい、その結果、(4)の実行時点で$selfがundefになってしまったからである。このように、コールバック関数の実行によってweakenした変数の指すオブジェクトがDESTROYされることは起こりうる。
余談ではあるが、_clear_next_readメソッドの中身をそのまま(4)に展開すると例外は発生しない。これはundefとなった$selfをHASHREFとして参照することでautovivificationが発生し、$self = {}と$self->{next_read_callbacks} = []が暗黙的に実行されているからであり、これはこれで厄介なバグを生む可能性がある。
これを防ぐためには(2)をコメントアウトを解除すればいい。
my $strong_self = $self;
すると実行結果は以下のようになる。
$ perl wrapper_user.pl
Received foobar
undef handle
Handle destroyed.
$strong_selfがon_readコールバック関数のスコープ内に存在することで、next_read_callbacks内の関数が何をしようとも実行中は$handleオブジェクトへの参照可能性が担保される。そのため、$selfはundefとならない。上記の例の場合、on_readコールバック関数が終了した後に$strong_selfは消滅するので、この時点で$handleは無事解放される。