読者です 読者をやめる 読者になる 読者になる

循環参照と弱い参照とScalar::Util::weaken

perl

参照カウンタ

次のようなコードを見ると、ちょっと不思議に思います。

use strict;
use warnings;

sub create_func {
    my ($name) = @_;
    
    my $func = sub {
        print "Hello, $name\n";
    };

    return $func;
}


my $func = create_func('memememomo');
$func->();


不思議に思うところは、create_func()の中の$nameです。
普段の感覚ですと、$nameはcreate_func()を抜けた後に解放されちゃうんじゃないかと思うのです。
しかし、まあ、そこはうまいことやっているんだろうな、ということでスルーしていました。


そのうまいことをやっている仕組みで使われているのが「参照カウンタ」です。
参照カウンタというのは、変数の値が参照されている箇所を数え上げているものです。
perlでは、参照カウンタが0になると、その変数を解放するようにしています。


上記のコードでは、"memememomo"のデータは、$nameという変数と$funcという変数で参照されています。
ですので、参照カウンタは2になります。
create_func()関数を抜けると、$nameの参照がなくなりますが、$func変数からの参照は引き続きます。
ですので、参照カウンタは1なので、$nameが参照していたデータは保持されるのです。
create_func();を抜けた後は、$funcの参照がなくなるまで保持されます。

なくなる場合というのは次のような場合です。

  • $func = undef;
  • プログラムが終了


こうすることで、参照カウンタが0になり、"memememomo"分のメモリは解放される事になります。


循環参照

参照がなくなれば、メモリが解放されます。
しかし、循環参照が起こると、メモリリークが起こることになります。
循環参照というのは、お互いに参照し合う状態のことです。
次のような場合も循環しています。

package ObjectA;
use strict;
use warnings;

sub new {
    my $class = shift;
    bless {}, $class;
}

sub print {
    my $self = shift;
    my ($number) = @_;
    print "test:$number\n";
}

sub create_func {
    my $self = shift;
    my ($number) = @_;

    my $func = sub {
        my $object = $self;
        $object->print($number);
    };

    $self->{func_list} ||= [];
    push @{$self->{func_list}}, $func;

    return $func;
}

package main;
use strict;
use warnings;

my $a = ObjectA->new;
my $func = $a->create_func(2);

$func->();


$a = undef;
$func = undef;


while(1) {
  # なんかの処理
}


create_func()の中で、コードリファレンスを作成し、ObjectAクラスのメンバ変数でそれを保持しています。
また、コードリファレンスの中でObjectAクラスのインスタンスを参照しています。
つまり、ObjectAとコードリファレンスの間で、お互いに参照し合っています。
その分、参照カウンタが1増えます。
mainパッケージでも、$aと$funcで参照しているので、それぞれ参照カウンタは2になります。
$func->();が呼び出された後、$aと$funcはundefされています。
これで、インスタンス分のメモリとコードリファレンス分のメモリが解放したいのですが、
この時点では参照カウンタが0になりません。
その後、その他の処理が続きます。しかし、メモリは解放されないままとなっています。
これは、プログラムが終了するまで解放されません。
これがメモリリークの原因となります。

弱い参照

循環参照を解決するためには、「弱い参照」を使用します。
弱い参照を行なうと、自己参照するときに参照カウンタを増やさないようになります。
ですので、参照カウンタが0にならずにメモリが解放されないような状況を回避できるようになります。
perlでは、弱い参照を使用するために「Scalar::Util::weaken()」を使用します。

Scalar::Util::weaken($self);


とやると、$selfの参照カウンタがインクリメントされなくなります。

参照カウンタを見る

参照カウンタが実際にどうなっているのかを見るために「Devel::Peek」というのがあります。

use strict;
use warnings;
use Devel::Peek;


my $name = 'memememomo';
Dump $name;


次のように出力されます。REFCNTの部分が参照カウンタですね。

SV = PV(0x100801c78) at 0x10082b4c0
REFCNT = 1
FLAGS = (PADMY,POK,pPOK)
PV = 0x1002028a0 "memememomo"\0
CUR = 10
LEN = 16