取势 明道 优术

作者为 扶 凯 发表

近来老见到人有内存泄漏的问题,自己写模块和例子的时候,也发现有内存泄漏的问题。。。学艺不精啊,所以特在这写一个文章来分享一下有关这方面的内容。

因为回调和闭包在事件程序中最多,所以我很早以前就找过一个有关这个的文章 <<AnyEvent and memory leaks >> 这个文章的作者,见到了 kraih (Mojolicious 的作者) 放了一个  gist link 上面一个简单的内存泄漏的例子。这指出了,对于初学者来讲,写这种事件回调程序时最容易出的错的地方,  所以对于我们写 AnyEvent (Mojolicious 之类的应用)之类的程序员,了解这块非常非常有用,这样我们对可能造成的内存泄漏之类问题就知道怎么应对和处理 .

闭包的内存泄漏实例

kraih 的例子如下:

# Very common leak
my $foo;
$foo = sub {
  my ($i, $j) = @_;
  return if $i >= $j;
  say $i++;
  $foo->($i, $j);
};
$foo->(1, 10);


测试是否内存泄漏的方式

对于上面这个例子,是一定有内存泄漏的,这时我们要怎么样来测试内存泄漏啦?

方法 1: 使用 Devel::Cycle 模块,来测试引用, 如上面的例子,我们可以在最后面加上

find_cycle $foo;

这样可以见到如下输出:

Cycle (1):
                      $A variable $foo => \$B                           
                                         $$B => \&A

上面可以直接见到变量一直存在对自身的引用,内存所以不被释放。

方法2:上面的方式太复杂是吧。。。因为有时是一个对象,有时你根本不知道什么地方出错,这时我们有个法子进行压力测试,这样来让问题暴露出来,我们还是拿上面的例子来看。

$0 = 'perl_MEM_TEST';

while(1) {
    {   
        my $foo;
        $foo = sub {
            my ($i, $j) = @_;
            return if $i >= $j;
            #say $i++;
            $foo->($i, $j);
        };  
        $foo->(1, 10);
    }   
}

嗯,我们运维的思想, 这时只要 ps 来检查内存占用就好。因为 while 所以每秒的次数会非常非常高。一会问题就出来了。

# ps -eo pid,user,comm,args,%cpu,rss,vsz|grep TEST|grep -v grep
10366 root     perl            perl_MEM_TEST                101 2094848 2310136

我们可以见到 rss,基本上几秒以后就快速的增长,然后接下来就简单了,一个个的注掉函数,不断的隔离函数,直到定位到出了问题的函数。

内存泄漏的原因

kraih 例子中的 bug 是因为 $foo 声明是在闭包之外而引起的. 但在函数内部的代码访问了这个闭包的变量。 这时变量的引用计数器增加 了。基本上所有的内存泄漏都是这个原因。从上面 find_cycle 都是这样。

在看一个例子,前几天我所犯下的错误,经 py 指正过的。我在 AnyEvent 的应用中的例子,我想使用 begin end 来做回调组的同步,例子的例如下:

  while(1) {

    my $cv = AE::cv;

    $cv->begin(sub{

        $cv->send;

    });

    my $w = AE::timer 0, 0, sub {

        $cv->end;

    };

    $cv->recv;

}

也可以使用上面的方法 2 来进行内存泄漏的测试,也能见到内存快速的增长,但大家见到上面的错误了吗? 嗯,这个错误很深,只是一个简单的错误。因为我在第一个 begin 中使用了 $cv ,但这个 $cv 是直接引用的外面的变量,所以让引用计数器在不断的增加。
其实作者也做了一样的的处理,这个时候,其实我们一定要使用传进来的 $cv 不要使用外部的 cv ,所以这个地方 $cv->send 必须要写成 shift->send 或者 $_[0]->send; 所以于对这种闭包的使用,我们最好都使用传参数的方式给所需要的函数引用传进去 。

纠正内存泄漏

对于 kraih 的例子中,要纠正这个函数,方式是象下面一样,给要调用的函数本身通过传参的方式来传进去,这是 <<AnyEvent and memory leaks>> 中提供的方式.

my $foo;
$foo = sub {
    my ($foofoo, $i, $j) = @_;
    return if $i >= $j;
    say $i++;
    $foofoo->($foofoo, $i, $j);
    undef $foofoo; #this line is optional
};

$foo->($foo, 1, 10);


我们使用传参的方式,给函数传进去后就一切正常了。但上面的方法并不是很好看和好懂,这种闭包多了,函数的层级也会非常多,比如 AnyEvent::HTTP 模块中,就大量使用了这种闭包。

下面提供一种我认为很好的方式,其实我其它的文章中也提到过.

    {   
        package T;
        use Moo;
        sub foo {
            my $self = shift;
            return sub {
                my ($i, $j) = @_;
                return if $i >= $j;
                say $i++;
                $self->foo->($i, $j);
            }   
        };  
        1;  
    }   

    my $t = T->new;
    $t->foo->(1, 10);

给需要的东西,包成模块,然后给闭包这种难懂的东西,封装起来。当然可能不一定要使用闭包,只是自己调用自己的话,就可以不用这样,上面对于于象 AnyEvent::HTTP  中的回调 on_body 之类这种需要一个代码引用的时候,非常有用,因为 $self->foo 是返回一个代码引用。可以直接

on_body => $self->foo;

这样很方便的使用,在需要大量回调的程序中,还不用给闭包堆得超级超级多,一层又一层。

来了就留个评论吧! 没有评论