POEは、Perlで複数の擬似プロセスを作るのが簡単です。Perlで複数の処理を別々に実行するには、forkを使うのが定番でした。
#!/usr/bin/perl
use strict;
use warnings;
my $pid = fork();
if ($pid) {
# parent
print "Hi, I'm parent. My PID is $$\n";
waitpid $pid, 0;
print "I'm parent. kid $pid exited.\n";
}
elsif ($pid == 0) {
# kid
print "Hi, I'm kid. My PID is $$\n";
}
else {
die "cannot fork() $!";
}
exit;
実行結果は次の様になります。
Hi, I'm parent. My PID is 88225
Hi, I'm kid. My PID is 88226
I'm parent. kid 88226 exited.
Perlのfork()はUnix環境だと、fork(2)システムコールを呼び出します。Perl固有の問題もいくつかありますが、やることはいわゆるUnixプログラミングと同じです。childプロセスはfork()以前の環境をparentと共有し、その後は別々の道を歩きます。
同じことをPOEでやるには、複数のsessionを作成します。
#!/usr/bin/perl
use strict;
use warnings;
use POE;
POE::Session->create(
inline_states => {
_start => sub {
my ($kernel, $session) = @_[ KERNEL, SESSION ];
$kernel->alias_set("alice");
print "Hi, I'm alice. My session ID is " . $session->ID . "\n";
},
_stop => sub {
print "I'm alice. Exiting.\n";
}
}
);
POE::Session->create(
inline_states => {
_start => sub {
my ($kernel, $session) = @_[ KERNEL, SESSION ];
$kernel->alias_set("bob");
print "Hi, I'm bob. My session ID is " . $session->ID . "\n";
},
_stop => sub {
print "I'm bob. Exiting.\n";
}
}
);
POE::Kernel->run();
exit;
実行結果は以下の様になります。
Hi, I'm alice. My session ID is 2
Hi, I'm bob. My session ID is 3
I'm alice. Exiting.
I'm bob. Exiting.
POEではいくつかのお約束がありますが、そのひとつが「fork()を使わないこと」です。POEではプロセスの代わりにsessionを作成します。sessionは、Unixのプロセスモデルによく似ていて、それぞれのsessionは独自のevent、ストレージ($_[HEAP])などを保持します。sessionにはUnixのプロセスIDと同じように、session IDが割り当てられ、それぞれのsessionを区別します。このsessionの分離はPOE::Kernelによってすべて処理され、プログラマは意識する必要がありません。
作成したsession同士で通信させてみます。
#!/usr/bin/perl
use strict;
use warnings;
use POE;
POE::Session->create(
inline_states => {
_start => sub {
my ($kernel, $session) = @_[ KERNEL, SESSION ];
$kernel->alias_set("alice");
print "Hi, I'm alice. My session ID is " . $session->ID . "\n";
},
got_message => sub {
my ($kernel, $session, $msg) = @_[ KERNEL, SESSION, ARG0 ];
print "(alice got message \"$msg\")\n";
if ( $msg =~ /how are you\?/i ) {
$kernel->post("bob", "got_message", "Fine.");
}
},
_stop => sub {
print "I'm alice. Exiting.\n";
}
}
);
POE::Session->create(
inline_states => {
_start => sub {
my ($kernel, $session) = @_[ KERNEL, SESSION ];
$kernel->alias_set("bob");
print "Hi, I'm bob. My session ID is " . $session->ID . "\n";
$kernel->post("alice", "got_message", "How are you?");
},
got_message => sub {
my ($kernel, $session, $msg) = @_[ KERNEL, SESSION, ARG0 ];
print "(bob got message \"$msg\")\n";
},
_stop => sub {
print "I'm bob. Exiting.\n";
}
}
);
POE::Kernel->run();
exit;
実行結果は以下の様になります。
Hi, I'm alice. My session ID is 2
Hi, I'm bob. My session ID is 3
(alice got message "How are you?")
(bob got message "Fine.")
I'm alice. Exiting.
I'm bob. Exiting.
新たにgot_messageというevent handlerを定義し、それぞれ受け取ったメッセージを表示し、aliceのほうは挨拶されたら返事を返します。これと似たような処理をfork()によるIPC(InterProcess Communication)では以下の様になります(Perl Cookbook 16.10より)。
use IO::Handle;
pipe(PARENT_RDR, CHILD_WTR);
pipe(CHILD_RDR, PARENT_WTR);
CHILD_WTR->autoflush(1);
PARENT_WTR->autoflush(1);
if ($pid = fork) {
close PARENT_RDR; close PARENT_WTR;
print CHILD_WTR "Parent Pid $$ is sending this\n";
chomp($line = <child_rdr>);
print "Parent Pid $$ just read this: `$line'\n";
close CHILD_RDR; close CHILD_WTR;
waitpid($pid,0);
} else {
die "cannot fork: $!" unless defined $pid;
close CHILD_RDR; close CHILD_WTR;
chomp($line = <parent_rdr>);
print "Child Pid $$ just read this: `$line'\n";
print PARENT_WTR "Child Pid $$ is sending this\n";
close PARENT_RDR; close PARENT_WTR;
exit;
}
ここではpipe()を使って相互に通信しています。このやり方では、お互いのメッセージを待つ間、処理がブロックしてしまいます。非同期に通信する方法もありますが、ややこしくて面倒です(このへんのテクニックは Lincoln D. Steinによる「Perlネットワークプログラミング」が詳しい)。また、通信内容にPerlのオブジェクトを含めたりすることはできません。ややこしいものをやりとりするには通信のためのプロトコルを定めて、お互いがそれを守る必要があります。
POEによる例では、postによってeventは非同期に処理されます。bobはaliceにメッセージをpostしたあとに別の処理を実行できます。postされたeventはPOE::Kernelがよきに計らってくれます。postする内容は任意のPerlデータを含められますので、objectを投げることだってできます。aliceはメッセージを受け取り、メッセージが挨拶であればbobに返事を返しています。postの引数は$_[ARG0]に入っています。複数の引数を受けとるには、ARG0、ARG1などを使うか、任意の引数を受けとるのであれば、@_[ARG0..$#_]を使います。
eventはsessionに送るmessageです。POE::Kernelはeventを発生させ、sessionはeventに応じて処理を実行します。session同士は、eventを通じて通信します。eventを送られたsessionはeventに応じたevent handlerを実行します。event handlerのことをPOEではstateと呼ぶことがあります。このeventとevent handlerを対応付けるのがinline_statesです。POE::Session->createの重要な役割は、この対応付けです。対応付けの方法はいくつかあり、coderefを使うinline_state、objectのmethodを対応させるobject_states、package methodを対応させるpackage_statesがあります。それぞれメリットデメリットがあります。
sessionはsession IDによって区別されますが、sessionにはわかりやすいaliasをつけることができます。それを行っているのが$kernel->alias_setです。これはUnixにおけるシステムコールみたいなもので、実際の処理はPOE::Kernelが行います。sessionの管理はすべてPOE::Kernelが面倒を見ます(こうした例は、POEが仮想OSと呼ばれる理由です)。aliasをつけるとeventを発行する対象のsessionのIDを意識せずにeventを発行できます。aliasとsession IDの名前解決もPOE::Kernelがよきに計らってくれます。