だいぶ習熟したつもりでも、いまだに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
は無事解放される。