2005-11-28

mod_perlの実験(3) - 変数の初期化問題

最後に変数の初期化に関する実験を取り上げます。

our変数の初期化問題

mod_perlでは、スクリプトやモジュールで使用するパッケージ変数の初期化が問題になります。例えば、次のモジュール(MyCounter.pm)について考えます。

MyCounter.pm

package MyCounter;

our $counter = 0; #変数の初期化

sub increment{
  $counter++;
}

モジュールの初期化コードで上のような変数の初期化を行っても意味がありません。なぜなら、この初期化コードは初回のリクエスト時にしか走らないからです。これは、前出の「Perlプログラムのライフサイクル」から考えても容易に想像できます。解決策は、リクエスト時に毎回走るスクリプトの初期処理として変数を初期化するしかありません。

my変数の初期化問題

変数の初期化に関する問題はパッケージ変数だけではありません。my変数の場合にも起こります。次のスクリプト(count1.cgi)について考えます。

※この題材は、下のURL内の「Exposing Apache::Registry secrets」(Apache::Registryの秘密を暴く)と同種です。
 http://perl.apache.org/docs/1.0/guide/porting.html
 CGIからmod_perlへの移植:mod_perlコーディングのガイドライン(mod_perl 1.0)

count1.cgi

#!/usr/bin/perl -w
use strict;
print "Content-type: text/plain\n\n";

my  $counter =0;
our $outer_ref = \$counter;

increment();

sub increment{
  $counter++;
  our $inner_ref = \$counter;
  print "Counter:$counter InnerRef:$inner_ref OuterRef:$outer_ref\n";
}

カウンタ($counter)をmy宣言で初期化した後、1回だけインクリメントして表示する簡単なスクリプトです。また、表示の際に、サブルーチンの外側から見たカウンタのリファレンスと、内側から見たリファレンスを表示しています。当然、CGIやModPerl::PerlRunで何回リクエストしても同じ結果になります。

CGI、ModPerl::PerlRunでの実行結果

Counter:1 InnerRef:SCALAR(0x1534294) OuterRef:SCALAR(0x1534294)
Counter:1 InnerRef:SCALAR(0x153421c) OuterRef:SCALAR(0x153421c)
Counter:1 InnerRef:SCALAR(0x1534258) OuterRef:SCALAR(0x1534258)

しかし、ModPerl::Registryの場合は、異なった結果になります。

ModPerl::Registryでの実行結果

Counter:1 InnerRef:SCALAR(0x7442f8) OuterRef:SCALAR(0x7442f8)
Counter:2 InnerRef:SCALAR(0x7442f8) OuterRef:SCALAR(0x7441b4)
Counter:3 InnerRef:SCALAR(0x7442f8) OuterRef:SCALAR(0x7444c0)

カウンタ($counter)はリクエスト毎に初期化されています。なぜ、このような結果になるのでしょうか?答えは、表示されたカウンタのリファレンスを見れば分かります。サブルーチンの外側から見たカウンタのリファレンスは毎回変化していますが、内側から見たリファレンスは1回目のリクエストから変化していません。

この問題はmy変数をネストされたサブルーチンの中で使用すると起こります。最初にサブルーチン(increment)が呼ばれた時に、外側で宣言されたmy変数($counter)のリファレンスが参照され、以降の呼び出しではこのリファレンスが引き続き使われます。「ネストされたサブルーチン中のmy変数」の問題はmod_perl固有の問題ではなくPerlの仕様です。ModPerl::Registryがスクリプトをメモリにキャッシュし続けているので、この問題が表面化しているにすぎません。「ネストされたサブルーチン中のmy変数」の仕様を知っていたとしても、ModPerl::Registryの場合は盲点になるかもしれません。

同種の問題はCGIの場合でも発生します。次のコードを見てください。前のスクリプト(count1.cgi)のmy宣言以下を1つのサブルーチン(MyCount)の中に入れて、このサブルーチンを3回呼び出しています。

count2.cgi

#!/usr/bin/perl -w
use strict;
print "Content-type: text/plain\n\n";

MyCount() foreach(1..3); 

sub MyCount{
  my  $counter =0;
  our $outer_ref = \$counter;

  increment();

  sub increment{
    $counter++;
    our $inner_ref = \$counter;
    print "Counter:$counter InnerRef:$inner_ref OuterRef:$outer_ref\n";
  }
}

CGIでの実行結果は、次のようになります。

CGIでの実行結果

Counter:1 InnerRef:SCALAR(0x153d898) OuterRef:SCALAR(0x153d898)
Counter:2 InnerRef:SCALAR(0x153d898) OuterRef:SCALAR(0x27501c)
Counter:3 InnerRef:SCALAR(0x153d898) OuterRef:SCALAR(0x275040)

このスクリプトを実行すると、以下の警告がApacheのエラーログの中に見つかります。

Variable "$counter" will not stay shared at D:/WWWRoot/mod_test/count2.cgi line 14. ・・・

これはサブルーチンの内側と外側でmy変数($counter )の共有が出来ていない事を示しています。

ここで取り上げた例題(count1.cgi)は実用的なものではありませんが、本当にサブルーチンの外側で宣言されたmy変数をサブルーチンの内側で使用したい場合は、無名サブルーチンを使ってクロージャを作ります。

count3.cgi

#!/usr/bin/perl -w
use strict;
print "Content-type: text/plain\n\n";

my  $counter =0;
our $outer_ref = \$counter; 

my $increment = sub{
  $counter++;
  our $inner_ref = \$counter;
  print "Counter:$counter InnerRef:$inner_ref OuterRef:$outer_ref\n";
};

&$increment();

このスクリプトを実行するとModPerl::Registryの場合でもCGIと同じ動作をします。クロージャは生成された時点の外側のmy変数を参照します。



最終更新のRSS Last-modified: Tue, 29 Nov 2005 07:55:57 JST (4257d)