Pixel Pedals of Tomakomai

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

クロージャとメモリリーク

WEBアプリでありそうなこんなコードがあります。flushでデータを排出するのですが、そのときに$app->run内で$app->{flush_fanc}に関数リファレンスを渡すと、$app->flushでそれを終了処理として呼びます。


package main;

{
my $app = App->new();
$app->run();
$app->flush();
}

print "end?n";

さて、Appクラスのrunを実装します。このとき、flush時に依頼する終了処理としてAppクラスの"メソッド"を使いたい場合、単純な関数リファレンスだとメソッドとして渡せないので、クロージャを渡してみます。ここでは、終了処理としてhelloメソッドを、きちんと$selfのメソッドとして呼び出したいとします。

package App;
sub new{bless{}, shift}

sub hello{print "hello?n"}

sub run{
my $self = shift;
#いろーーんな処理 ===
#...
#終了時にやりたい処理があるので、依頼
$self->{flush_func} = sub {
#クロージャとして、メソッドを実行する
$self->hello();
}
}

sub flush{
my $self = shift;
$self->{flush_func}->();
}

sub DESTROY{print "DESTROYED?n"}

1;

はい、こんな感じで。なぜかDESTROYがあるのは、実行結果をみるため。実行結果は次のような感じです。


hello
end
DESTROYED

気づいたでしょうか? $appのスコープはブロック内なので、本来はendの前にDESTROYEDと表示されなければならないのに、最後に表示されている。これは、$appが破壊されずにプログラムの終了まで生き延びた、つまり、リークしてると言うことです。

するどい人ならこの結果を見る前に気がついたと思いますが、クロージャに$selfを使っていて、それを$selfのメンバとして保存しているため、循環参照が起こっているわけです。

でもって、この解決法。恥ずかしながら昨日初めて知ったのですが、Scalar::Util::weakenなんていうなんとも破天荒な関数が居ます。これは、参照カウンタを1減らすと言うもの*1


$self->{flush_func} = sub {
$self->hello();
Scalar::Util::weaken($self);
}

これでめでたくクロージャ内の$selfは弱いリファレンスとなり、循環参照によるリークを防げるようになります。

*1:ほんとはカウンタを増やさないリファレンスだが、コードを書いてて「減らす」と考えた方が直感的かな、と。