理系学生日記

おまえはいつまで学生気分なのか

POE の処理の流れを把握してみる

メモ書き程度に。

準備

POE の多くのクラスは、POE::Kernel に対する mix-in となっています。つまり、POE::Kernel パッケージに属するメソッドなのに、Kernel.pm にないんだけど…!! ということがよく起こる。これをなんとかするために、とりあえず TAGS ファイルを作っておきます。

$ DIR=/opt/local/lib/perl5/site_perl/5.10.0/
$ etags $DIR/POE.pm `find $DIR/POE -type f`

mix-in なので、基本的にはメソッドの多くが POE::Kernel に属しますが、ここでは便宜上、POE/hoge.pm に書かれているメソッドは POE::hoge::method と記述します。

主な流れ

POE の主な処理は、POE::Kernel::run の呼び出しから始まります。POE::Kernel::run は前処理を行った後で、loop_run を呼び出します。
この loop_run は、POE::Loop のサブクラスで定義されています。POE ではイベントループを実行するモジュールを、POE::Kernel とは別にしていて、そのループを担当する抽象クラスが POE::Loop となっています。デフォルトでは、POE::Loop::Select が選ばれるようです。

POE::Loop::Select の loop_run は、表面上、非常に単純なイベントループを構成しています。とはいえ、イベントループってだいたいこういう形になるものだとは思いますが。

sub loop_run {
  my $self = shift;

  # Run for as long as there are sessions to service.
  while ($self->_data_ses_count()) {
    $self->loop_do_timeslice();
  }
}

ここでは、POE::Session が残っている限り、ループ処理を実行します。

POE::Loop::Select::loop_do_timeslice では、いわゆる select(2) を呼び出し、監視対象となるファイルハンドルの変化をチェックしたりしています(もちろん、監視対象となるファイルディスクリプタを指定した場合ですが)。

      # Check filehandles, or wait for a period of time to elapse.
      my $hits = CORE::select(
        my $rout = $loop_vectors[MODE_RD],
        my $wout = $loop_vectors[MODE_WR],
        my $eout = $loop_vectors[MODE_EX],
        $timeout,
      );

ここでの select(2) の呼び出しは、sleep 呼び出しと同じ意味合いも持っています。つまり、次のイベントが発生するまで一定時間待機するということです(もちろん、その待機中にファイルディスクリプタに変化があれば、select(2) が返ってきます)。
ここでは、何らかの変化、たとえば監視対象となるファイルディスクリプタへの書き込みが起こると、イベントキューにその旨を追加するのですが、今回は何も起こらず、select(2) がタイムアウトして返ってきた場合を考えます。

select(2) に渡すタイムアウト値は、次のイベントが発生するまでの時間を表しています。つまり、select(2) がタイムアウトしたという状態は、POE が何らかのイベントを処理する時間であることを意味しているわけで、POE::Loop::Select::loop_do_timeslice ではメソッド POE::Resource::Events::_data_ev_dispatch_due を呼びだします。

  # Dispatch whatever events are due.
  $self->_data_ev_dispatch_due();

呼び出される POE::Resource::Events::_data_ev_dispatch_due イベントキューから次に起こる event を取得し、POE::Kernel::_dispatch_event を呼び出します。

  while (defined($next_time = $kr_queue->get_next_priority())) {
    last if $next_time > $now;

    my ($due_time, $id, $event) = $kr_queue->dequeue_next();
    # 略

    $self->_data_ev_refcount_dec($event->[EV_SOURCE], $event->[EV_SESSION]);
    $self->_dispatch_event(@$event, $due_time, $id);

そのイベントはシグナル処理だったり、select(2) によって検知したファイルディスクリプタの変化イベントだったりしますが、今回は一番スタンダードなイベントを追ってみます。

実はこの段階(POE::Kernel::_dispatch_event)で、イベントをどの POE::Session に送らないといけないかは、POE::Kernel::_dispatch_event には分かっています(POE::Resource::Events::_data_ev_dispatch_due から渡されます)。POE::Kernel はその POE::Session の _invoke_state を呼び出します。POE::Session::_invoke_state では実際に指定されたセッションの指定された状態におけるコードリファレンスを実行します。呼び出しはこんな感じ(inline_state の場合)。

  if (ref($self->[SE_STATES]->{$state}) eq 'CODE') {
    return $self->[SE_STATES]->{$state}->
      ( undef,                          # object
        $self,                          # session
        $POE::Kernel::poe_kernel,       # kernel
        $self->[SE_NAMESPACE],          # heap
        $state,                         # state
        $source_session,                # sender
        undef,                          # unused #6
        $file,                          # caller file name
        $line,                          # caller file line
        $fromstate,                     # caller state
        @$etc                           # args
      );
  }