« 2010年6月 | トップページ | 2010年8月 »

2010年7月14日 (水)

デザインパターン Factory method (php&ruby)

デザインパターンのFactory methodは、インスタンスするオブジェクトをサブクラスが決めます。インスタンスされるオブジェクトはインターフェースクラスによりI/Fを規定されているため、共通のAPIが実装されます。
このことにより、インスタンスするオブジェクトの種類が増えても、インスタンスの方法をサブクラスに集約するだけで済みます。


たとえば、とあるバッチ処理が排他制御を仕様としてもっているとします。排他制御を実現するために、ここではふたとおりの方法を提案します。

ひとつは、実行開始時にロックファイルを用意して、実行完了時にロックファイルを削除する方法です。実行開始時にロックファイルを検知すると、他に実行中のプロセスがあると判断して処理を中断しますが、いちいちディスクIOが発生します。

もうひとつは、実行開始時にプロセスIDを確認して、カレントのプロセスID以外に実行しているものがあれば多重実行と判断して処理を中断する方法です。こちらの方が高速でIOも発生しませんが、セキュリティによってシステムコールが制限されているとか、ホームディレクトリより上を参照できないような環境ではうまく動かない可能性があります。

汎用性を考慮して、両方の機能を環境条件によって使い分けられるようにしておきたいと思います。

普通にコーディングすると、こういう場合は利用側のスクリプト内に環境条件による場合わけをif文やswitch文などを使って、インスタンスするオブジェクトを選べるようにしていくと思います。
しかしこれだとインスタンスする種類が増えた場合に可読性が落ちていきますし、インスタンスされるオブジェクトのインターフェースが統一されていないと、利用時にまた場合わけしていかなければならなくなります。

Fatcory methodを使うと、場合わけはそれを専門におこなうサブクラスの一箇所に集中できるので、運用中の変更であっても他に予期せぬ影響が発生しづらくなります。
また、インスタンスされるオブジェクトは共通のAPIを持つようインターフェースクラスによる拘束を受けるため、利用側から見ると、どのオブジェクトであっても使用方法が同じとなり、利用側のソースに手を加える必要がなくなります。


ともあれ百聞は一見にしかず。クラス図をみてみまょう。

Factory_method_pattern_2

インスタンスするオブジェクトを条件によって場合わけするサブクラスは、この図の左下のLockFactoryクラスです。どういうロック(排他制御)を提供するかは、このLockFactoryクラスだけが知っています。
逆に言えば、ロックの選定方法を修正したい場合は、ここだけ手を加えればよい、ということになります。

一方、インスタンスされるオブジェクトは右下のLockFileControlクラス(ファイルによる排他制御)とLockPidControlクラス(プロセスIDによる排他制御)になります。このふたつのクラスはインターフェースクラスのProductクラスに、実装しなければならないインターフェースの拘束を受けているため、必然的に共通のインターフェース(listen()、exec()、close())が実装されることになります。

たとえばここに、実行履歴の管理も加えたいからTokyoCabinetを利用して排他制御をおこなおう、という仕様変更が発生しても、LockTcControlクラスのようなものを新規で用意して、その採用条件をLockFactoryクラスに追加えてあげればよい、ということになります。

Factoryは工場という意味なので、ここでは「なにをつくるか」だけに責任を持ち、「どうやってつくるか」は責任範囲外です。
Productは生産品という意味なので、ここでは「どうやってつくるか」という、その生産品を生産品たらしめん構成要素だけに着目します。

たとえばお菓子工場であれば、クッキーやチョコレートを並行製造していますが、クッキーやチョコレートそれ自体は自分以外にどんなお菓子がつくられているかということは知っておく必要はありません。クッキーはクッキーたらしめん構成要素として、小麦粉や砂糖の含有率や使用される香料に責任を持っていればいいわけです。
一方、工場はクッキーがどんな香料を使用されているか管理する必要はありません。クッキーのつもりでチョコレートを作らないよう、条件にそった生産ラインを維持すればよい、ということになります。

クッキーやチョコレートは、今回のケースでいうところのファイルによる排他制御か、プロセスIDによる排他制御か、ということになります。その排他制御の実現方法はそれぞれのクラスだけが知っていればいいことです。
一方、場合わけクラスは工場として、その環境条件に合致した排他制御を提供してあげればよい、ということです。

こうしてお互いの責任範囲を明確にしてソースコードを分けることで、依存関係が緩くなり(疎結合)、改修や機能拡張に強い仕組みになります。
こうした仕組みでつくられたシステムを、堅牢性の高いシステムと呼び、顧客やエンドユーザ、しいては開発者を幸せにしていきます。


長くなりました。
さっそく実際のコードをみてみましょう。例によってphpとrubyで実装しています。

まずphpです。

Factory.class.php (Factoryクラス)

<?php
Abstract class Factory
{
public final function create(Array $argv) {
return $this->createProduct($argv);
}

protected abstract function createProduct(Array $argv);
}


LockFactory.class.php (LockFactoryクラス)

<?php
Class LockFactory extends Factory
{
const FILE_MODE = 1;
const PID_MODE = 2;

protected function createProduct(Array $argv) {
$mode = 1;
if (isset($argv[1]) && intval($argv[1]) > 0) {
$mode = intval($argv[1]);
}

switch ($mode) {
case self::FILE_MODE:
$lock = new LockFileControl($argv[0]);
break;

case self::PID_MODE:
$lock = new LockPidControl($argv[0]);
break;

default:
echo "invalid mode: ${mode}\n";
exit;
}
return $lock;
}
}


Product.class.php (Productクラス)

<?php
Interface Product
{
public function listen();
public function exec();
public function close();
}

LockFileControl.class.php (LockFileControlクラス)

<?php
Class LockFileException extends Exception {}

Class LockFileControl implements Product
{
const LIMIT_TERM = 10;
private $_file = null;
private $_limit_term = null;

public function __construct($file, $limit_term = null) {
$this->_file = $file . '.lock';
$this->_limit_term = $limit_term ? $limit_term : self::LIMIT_TERM;
}

public function listen() {
if (($work_time = $this->_getWorkTime()) !== false) {
if ($work_time > $this->_limit_term) {
try {
$ret = $this->close();
} catch (LockFileException $e) {
throw new LockFileException('deadlock: ' . $e->getMessage());
}
if (! $ret) {
throw new LockFileException('deadlock: ' . $this->_file);
}
return false;
}
}
return (bool)$work_time;
}

public function exec() {
$ret = touch($this->_file);
if (! $ret) {
throw new LockFileException('failed to lock: ' . $this->_file);
}
return true;
}

public function close() {
$ret = unlink($this->_file);
if (! $ret) {
throw new LockFileException('failed to unlock: ' . $this->_file);
}
return true;
}

private function _getWorkTime() {
clearstatcache();
if (! is_file($this->_file)) {
return false;
}
return time() - filemtime($this->_file);
}
}

LockPidControl.class.php (LockPidControlクラス)

<?php
Class LockPidException extends Exception {}

Class LockPidControl implements Product
{
const LIMIT_TERM = 10;
private $_pname = null;
private $_limit_term = null;
private $_pid = null;

public function __construct($pname, $limit_term = null) {
$this->_pname = $pname;
$this->_limit_term = $limit_term ? $limit_term : self::LIMIT_TERM;
}

public function listen() {
if (($work_time = $this->_getWorkTime()) !== false) {
if ($work_time > $this->_limit_term) {
exec("kill -KILL " . $this->_pid);
if ($this->_getWorkTime()) {
throw new LockFileException('deadlock: ' . $this->_pid);
}
return false;
}
}
return (bool)$work_time;
}

public function exec() {
return true;
}

public function close() {
return true;
}

private function _getWorkTime() {
$cmd = "ps x | grep " . $this->_pname . " | grep -v grep | awk '{print \$1}'";
exec($cmd, $pids);
if (count($pids) == 1 && intval($pids[0]) == getmypid()) {
return false;
}
$pids = array_diff($pids, array(getmypid()));
$this->_pid = intval(array_shift($pids));
return time() - filemtime('/proc/' . $this->_pid);
}
}

factory_method_client.php (利用側)

<?php
require 'Factory.class.php';
require 'LockFactory.class.php';
require 'Product.class.php';
require 'LockFileControl.class.php';
require 'LockPidControl.class.php';

$factory = new LockFactory();
$lock = $factory->create($argv);

try {
if ($lock->listen()) {
echo "quit executing!\n";
exit;
}

if ($lock->exec()) {
echo "start working...\n";
#job some
sleep(5);
}

if ($lock->close()) {
echo "...end working\n";
}

} catch (Exception $e) {
echo $e->getMessage() . "\n";
}


phpは以上です。

次にrubyを見てみましょう。

Factory.class.rb (Factoryクラス)

class NotImplements < Exception;end

class Factory
def create(argv)
createProduct(argv)
end

def createProduct(argv)
raise NotImplements
end
protected :createProduct
end

LockFactory.class.rb (LockFactoryクラス)

class LockFactory < Factory
FILE_MODE = 1
PID_MODE = 2

def createProduct(argv)
mode = 1
mode = argv[1].to_i if argv[1].to_i > 0

case mode
when FILE_MODE
lock = LockFileControl.new(argv[0])

when PID_MODE
lock = LockPidControl.new(argv[0])

else
print "invalid mode: #{mode}\n"
exit
end
lock
end
protected :createProduct
end

Product.class.rb (Productクラス)

class NotImplements < Exception;end

module Product
def listen
raise NotImplements
end

def exec
raise NotImplements
end

def close
raise NotImplements
end
end

LockFileControl.class.rb (LockFileControlクラス)

class LockFileException < Exception;end

class LockFileControl
include Product

LIMIT_TERM = 10

def initialize(file, limit_term = nil)
@_file = file + '.lock'
@_limit_term = limit_term ? limit_term : LIMIT_TERM
end

def listen
if (work_time = getWorkTime) != false
if work_time > @_limit_term
begin
ret = close
rescue
raise "deadlock: #{$!}"
end
unless ret
raise "deadlock: #{@_file}"
end
return false
end
end
return false if work_time == 0 || work_time == false
true
end

def exec
begin
File.open(@_file, 'w').close
rescue
raise "failed to lock: #{@_file}"
end
true
end

def close
begin
File.unlink(@_file)
rescue
raise "failed to unlock: #{@_file}"
end
true
end

def getWorkTime
return false unless File.exist?(@_file)
Time.now.to_i - File.mtime(@_file).to_i
end
private :getWorkTime
end

LockPidControl.class.rb (LockPidControlクラス)

class LockPidException < Exception;end

class LockPidControl
include Product

LIMIT_TERM = 10

def initialize(pname, limit_term = nil)
@_pname = pname
@_limit_term = limit_term ? limit_term : LIMIT_TERM
@_pid = nil
end

def listen
if (work_time = getWorkTime) != false
if work_time > @_limit_term
`kill -KILL #{@_pid}`
raise "deadlock: #{@_pid}" if getWorkTime
return false
end
end
return false if work_time == 0 || work_time == false
true
end

def exec
true
end

def close
true
end

def getWorkTime
ret = `ps x | grep #{@_pname} | grep -v grep | awk '{print \$1}'`
pids = ret.split
return false if pids.size == 1 && pids[0].to_i == $$
pids = pids - [$$]
@_pid = pids.shift
Time.now.to_i - File.mtime("/proc/#{@_pid}").to_i
end
private :getWorkTime
end

factory_method_client.rb (利用側)

require 'Factory.class.rb'
require 'LockFactory.class.rb'
require 'Product.class.rb'
require 'LockFileControl.class.rb'
require 'LockPidControl.class.rb'

factory = LockFactory.new
lock = factory.create(ARGV.unshift(__FILE__))

begin
if lock.listen
print "quit excuting!\n"
exit
end

if lock.exec
print "start working...\n"
#job some
sleep 5
end

if lock.close
print "...end working\n"
end

rescue
print "#{$!}\n"
exit
end


ruby は以上です。


排他制御の方法を選定する環境条件は、ここではコマンドラインに与えた引数としています。1でファイルによる排他制御、2でプロセスIDによる排他制御です。
一度実行すると、このバッチ処理は実行時間に5秒を要します。そのあいだの多重実行は排他制御により防止されます。デッドロック判定は10秒です。

phpの実行結果: (rubyも結果は一緒)


php ./factory_method_client.php 1 &
[1] 22747
start working...

php ./factory_method_client.php 1 &
[2] 22748
quit executing!
[2]+ Done php ./factory_method_client.php 1

...end working
[1]+ Done php ./factory_method_client.php 1


php ./factory_method_client.php 2 &
[1] 22750
start working...

php ./factory_method_client.php 2 &
[2] 22756
quit executing!
[2]+ Done php ./factory_method_client.php 2

...end working
[1]+ Done php ./factory_method_client.php 2

まとめ:
そしてrubyってabstractとかfinalとかの修飾子って使えないんでしょうか。ちょっと調べてもよく分からなかったのでスルーしました。アジャイル開発向けということで僕自身もアジャイルに(笑)。

あと、開発時にシステム要件って決まってると思うので、あんまりこういう例のような使い方しないかもしれませんね。でも自分のつくったモジュールをいろんな開発案件で使いまわして開発コストをさげる、という意味では有効だと思います。ファイルによる排他制御しか使わないなら、LockFactoryクラスの条件分岐を固定してしまえばいいわけですから。

排他制御だけでなく、他にも複数の読み込むファイル形式による違い、複数のHTTP通信先による違い、複数の課金APIによる違い、などで加工方法が分かれるような場合にもこのパターンが使えます。
某ISPの基幹システムでもこれが使われていましたが、パターンを知っていると暗黙の共通語として機能するので解析やソースリーディングの助け舟にもなってくれますね(^^)

| | コメント (0) | トラックバック (0)

2010年7月11日 (日)

デザインパターン State (php&ruby)

デザインパターンのStateは、「状態」をクラスで管理します。
たとえば、ログインしている状態、ログインしていない状態、という2種類の「状態」があります。
それらの状態によって振る舞いが異なるとき、別々のクラスに書き分けることによって、利用側は状態の変更を通知するだけでよく、クラス側は状態によって条件分岐のロジックを仕込まずに済むようになります。

ここでは、実際にログインしている|していない、の状態をデザインパターンのStateを使って、phpとrubyでそれぞれ実装してみます。

まず、クラス図をみてみましょう。
State_pattern_2

Userクラスは利用側でインスタンスされる唯一のクラスです。このクラスは内部に「状態」オブジェクト(UserState)を保持していて、ポリモーフィズムを使った共通のインターフェース(isAuth(), getMenu(), doEdit(), doExit())を中継します。状態を切り替えるためのswitchState()メソッドが叩かれると、ラッピングされたnextStage()インターフェースを経由して、内部に保持した「状態」オブジェクトをまるごと入れ替えます。

UserStateクラスは、「状態」を表すクラスに共通のインターフェースを付与するためのインターフェースクラスです。

AuthStateクラスUnauthStateクラスは、それぞれログインしている|していない、を表すクラスです。
着目したいのは、nextStage()メソッドが、前者ではログインしていない状態オブジェクト(UnauthState)を、後者ではログインしている状態オブジェクト(AuthState)を返却していることです。これは、受け取る側のUserクラスからみると、「状態」がまるごと入れ替わることを意味します。
これによって、開発者は状態の違いによる固有な振る舞いを、それぞれのクラスに記述することだけに集中することができます。

いよいよコードをみてみましょう。
まずphpです。

User.class.php (Userクラス)

<?php
Class User
{
private $_name = null;
private $_state = null;

public function __construct($name) {
$this->_name = $name;
$this->_state = UnauthState::getInstance();
}

public function switchState() {
$this->_state = $this->_state->nextStage();
}

public function isAuth() {
return $this->_state->isAuth();
}

public function getMenu() {
return $this->_state->getMenu();
}

public function doEdit() {
return $this->_state->doEdit();
}

public function doExit() {
return $this->_state->doExit();
}

public function getName() {
return $this->_name;
}
}

UserState.class.php (UserStateクラス)

<?php
Interface UserState
{
public function isAuth();
public function nextStage();
public function getMenu();
public function doEdit();
public function doExit();
}

AuthState.class.php (AuthStateクラス)

<?php
Class AuthState implements UserState
{
private static $_singleton = null;

private function __construct() {
}

public static function getInstance() {
if (self::$_singleton == null) self::$_singleton = new AuthState();
return self::$_singleton;
}

public function isAuth() {
return true;
}

public function nextStage() {
return UnauthState::getInstance();
}

public function getMenu() {
echo "now login...\n 1: edit\n 2: logout\n input number: ";
return intval(chop(fgets(STDIN)));
}

public function doEdit() {
echo "edit...";
return true;
}

public function doExit() {
return false;
}

public final function __clone() {
throw new RuntimeException('cannot make clone: ' . get_class($this));
}
}

UnauthState.class.php (UnauthStateクラス)

<?php

Class UnauthState implements UserState
{
private static $_singleton = null;

private function __construct() {
}

public static function getInstance() {
if (self::$_singleton == null) self::$_singleton = new UnauthState();
return self::$_singleton;
}

public function isAuth() {
return false;
}

public function nextStage() {
return AuthState::getInstance();
}

public function getMenu() {
echo "now logout...\n 2: login\n 3: exit\n input number: ";
return intval(chop(fgets(STDIN)));
}

public function doEdit() {
return false;
}

public function doExit() {
echo "bye\n";
return true;
}

public final function __clone() {
throw new RuntimeException('cannot make clone: ' . get_class($this));
}
}

state_client.php (利用側)

<?php
require 'User.class.php';
require 'UserState.class.php';
require 'AuthState.class.php';
require 'UnauthState.class.php';

define('MODE_EDIT', 1);
define('MODE_STATE', 2);
define('MODE_EXIT', 3);

$context = new User('hoge');
$mode = null;

while (true) {
switch ($mode) {
case MODE_STATE :
$context->switchState();
if ($context->isAuth()) echo 'welcome ' . $context->getName() . "\n";
break;

case MODE_EDIT :
if ($context->doEdit()) echo "OK\n";
break;

case MODE_EXIT :
if ($context->doExit()) exit();
break;
}
$mode = $context->getMenu();
}

次にrubyです。

User.class.rb (Userクラス)

class User
def initialize(name)
@_name = name
@_state = UnauthState.instance
end

def switchState
@_state = @_state.nextStage
end

def isAuth
@_state.isAuth
end

def getMenu
@_state.getMenu
end

def doEdit
@_state.doEdit
end

def doExit
@_state.doExit
end

def getName
@_name
end
end

UserState.class.rb (UserStateクラス)

class NotImplements < Exception;end

module UserState
def isAuth
raise NotImplements
end

def nextStage
raise NotImplements
end

def getMenu
raise NotImplements
end

def doEdit
raise NotImplements
end

def doExit
raise NotImplements
end
end

AuthState.class.rb (AuthStateクラス)

class AuthState
include UserState
include Singleton

def isAuth
true
end

def nextStage
UnauthState.instance
end

def getMenu
print "now login...\n 1: edit\n 2: logout\n input number: "
STDIN.gets.chomp.to_i
end

def doEdit
print "edit..."
true
end

def doExit
false
end
end

UnauthState.class.rb (UnauthStateクラス)

class UnauthState 
include UserState
include Singleton

def isAuth
false
end

def nextStage
AuthState.instance
end

def getMenu
print "now logout...\n 2: login\n 3: exit\n input number: "
STDIN.gets.chomp.to_i
end

def doEdit
false
end

def doExit
print "bye\n"
true
end
end

state_client.rb (利用側)

require 'singleton'
require 'User.class.rb'
require 'UserState.class.rb'
require 'AuthState.class.rb'
require 'UnauthState.class.rb'

MODE_EDIT = 1
MODE_STATE = 2
MODE_EXIT = 3

context = User.new('hoge')
mode = nil

while(true)
case mode
when MODE_STATE
context.switchState
print "welcome #{context.getName}\n" if context.isAuth

when MODE_EDIT
print "OK\n" if context.doEdit

when MODE_EXIT
exit if context.doExit

end
mode = context.getMenu
end

php ./state_client.php | ruby ./state_client.rb (php&ruby共通の実行結果)

now logout...
2: login
3: exit
input number: 2
welcome hoge
now login...
1: edit
2: logout
input number: 1
edit...OK
now login...
1: edit
2: logout
input number: 2
now logout...
2: login
3: exit
input number: 3
bye

まとめ:
サービスをモデル化してクラスに落とし込む際に、どうしても名詞抽出法に頭がとらわれると、なかなか概念モデルをクラスにあてはめるという機知が働かないのですが、こういったデザインパターンをおさえておくと、そのサービスにとって主要で明確な振る舞いを持つ概念をクラス化してみることで、ずいぶんとif文やswitch文が減らせて可読性があがり、如いては堅牢性や汎用性も高まるという恩恵を受けられるかもしれませんね。
デザインパーンでは他にも「オブジェクト」にとらわれないモデリングの仕方が多々あります。機会があれば別のパターンも紹介できればと思います。

phpとrubyを並べて書くと、やっぱりrubyの方が楽で早いわあ、と感じます。シングルトンなんてモジュールで提供されているので、phpみたいに自前でこしらえる必要がありません。アジャイル開発なら断然phpよりrubyですね。

なお、phpはversion5.3.1、rubyはversion1.9.1を使用しています。
動作結果はシェルからコマンドとして実行したものです。

参考書籍: PHPによるデザインパターン入門

| | コメント (0) | トラックバック (0)

2010年7月 6日 (火)

phpのcurl_multi, phpのPCNTL, perlのithread, rubyのthreadで、並列処理パフォーマンス比較

phpのcurl_multiを使うとHTTP通信を並列でおこなってくれます。

最初に思ったのは、実は内部的にプロセス制御をおこなって子プロセスに通信処理をさせているのではないかということです。であればPCNTL関数から冗長性を削いだもの、かと思ったのですが、もしそうなら機能をHTTP通信に特化する必要はないですね。
むしろ内部的にはバックグラウンドで非同期通信をおこなって並列処理を再現しているのでしょうか。curlだし、そっちのような気がしてきました。

スクリプトを書いて試してみました。
それと、ついでに他のスクリプト言語の並列処理とパフォーマンス比較もしてみました。
測定したパフォーマンスは、消費時間とそのプロセスの消費メモリ(RSS)です。phpの場合の消費メモリは子プロセスが消費したぶんも加えています。
また、通信先ではスリープをいれて、取得に1秒かかる程度のデータ、を再現しています。これを10本同時におこなっているので、直列処理でおこなえばそれだけで10秒かかる内容です。
 

HTTP通信を並列処理

0. 共通の通信先データ (sleep.php)

<?php
header('HTTP1/0 200 OK', false, 200);
sleep(1);
echo 'OK: code=' . (isset($_GET['code']) ? intval($_GET['code']) : 0);

1. php curl_multi版 (chk.php)

<?php
$term = microtime(true);
$domain = 'localhost';
$path = '/sleep.php';
$count = 10;
$rets = array();
$conns = array();
$cpid_mem = 0;

$selfpid = intval(chop(`ps alx | grep chk.php | grep -v grep | awk '{print $3}'`));
$mh = curl_multi_init();

for ($i = 0; $i < $count; $i ++) {
$conns[$i] = curl_init("http://${domain}${path}?code=${i}");
curl_setopt($conns[$i], CURLOPT_RETURNTRANSFER, true);
curl_multi_add_handle($mh, $conns[$i]);
}

$running = null;
do {curl_multi_exec($mh, $running);} while ($running);

foreach (explode("\n", chop(
`ps alx | grep chk.php | grep -v grep | grep ${selfpid} | awk '{print $4, $8}'`
)) as $ps) {
$childs = explode(' ', $ps);
if ($childs[0] == $selfpid) $cpid_mem += intval($childs[1]);
}

foreach ($conns as $conn) {
array_push($rets, curl_multi_getcontent($conn));
curl_multi_remove_handle($mh, $conn);
curl_close($conn);
}

curl_multi_close($mh);

print_r($rets);
echo 'time: ' . round(microtime(true) - $term, 3) . "sec\n";
$ppid_mem = intval(chop(`ps alx | grep chk.php | grep -v grep | awk '{print $8}'`));
echo 'RSS: ' . $ppid_mem . 'KB(parent) + ' . $cpid_mem . "(childs)KB\n";

実行結果:

php -v | head -n 1
PHP 5.3.1 (cli) (built: Feb 6 2010 01:50:38)
php ./chk.php
Array
(
[0] => OK: code=0
[1] => OK: code=1
[2] => OK: code=2
[3] => OK: code=3
[4] => OK: code=4
[5] => OK: code=5
[6] => OK: code=6
[7] => OK: code=7
[8] => OK: code=8
[9] => OK: code=9
)
time: 1.048sec
RSS: 11412KB(parent) + 0(childs)KB

2. php PCNTL版 (chk2.php)

<?php
$term = microtime(true);
$domain = 'localhost';
$path = '/sleep.php';
$count = 10;
$rets = array();
$conns = array();
$cpid_mem = 0;
$cpid_cnt = 0;

declare(ticks=1);
$selfpid = intval(chop(`ps alx | grep chk2.php | grep -v grep | awk '{print $3}'`));

for ($i = 0; $i < $count; $i ++) {
$cpid = pcntl_fork();
if ($cpid) {
$cpid_cnt ++;
continue;
}

$ret = file_get_contents("http://${domain}${path}?code=${i}");
$fp = fopen('ret.txt', "a");
flock($fp, LOCK_EX);
fwrite($fp, $ret . "\n");
flock($fp, LOCK_UN);
fclose($fp);

exit;
}

foreach (explode("\n", chop(
`ps alx | grep chk2.php | grep -v grep | grep ${selfpid} | awk '{print \$4, \$8}'`
)) as $ps) {
$childs = explode(' ', $ps);
if ($childs[0] == $selfpid) $cpid_mem += intval($childs[1]);
}

while($cpid_cnt > 0) {
$cpid = pcntl_wait($stat);
$cpid_cnt--;
}

print_r(file_get_contents('ret.txt'));
echo 'time: ' . round(microtime(true) - $term, 3) . "sec\n";
$ppid_mem = intval(chop(`ps alx | grep chk2.php | grep -v grep | awk '{print $8}'`));
echo 'RSS: ' . $ppid_mem . 'KB(parent) + ' . $cpid_mem . "KB(childs)\n";

unlink('ret.txt');

実行結果:

php ./chk2.php
OK: code=3
OK: code=0
OK: code=1
OK: code=2
OK: code=4
OK: code=6
OK: code=7
OK: code=8
OK: code=5
OK: code=9
time: 1.149sec
RSS: 10752KB(parent) + 51876KB(childs)

3. perl スレッド版 (chk.pl)

use strict;
use threads;
use LWP::UserAgent;
use Data::Dump qw(dump);
use Time::HiRes qw(gettimeofday);

my $term = Time::HiRes::time;
my $domain = 'localhost';
my $path = '/sleep.php';
my $count = 10;
my @rets = ();
my @ts = ();

for (my $i = 0; $i < $count; $i ++) {
push(@ts, threads->new(sub {
my $req = new HTTP::Request GET => "http://$domain$path?code=$i";
my $res = new LWP::UserAgent->request($req);
threads->yield();
return $res->content;
}));
}
foreach (@ts) {push(@rets, $_->join);}

print dump(@rets) . "\n";
printf("time: %1.3fsec\n", Time::HiRes::time - $term);
print 'RSS: ' . `ps alx | grep chk.pl | grep -v grep | awk '{print \$8}'`;

実行結果:

perl -v | head -n 2

This is perl, v5.8.8 built for i386-linux-thread-multi
perl ./chk.pl
(
"OK: code=0",
"OK: code=1",
"OK: code=2",
"OK: code=3",
"OK: code=4",
"OK: code=5",
"OK: code=6",
"OK: code=7",
"OK: code=8",
"OK: code=9",
)
time: 1.313sec
RSS: 31024

4. ruby スレッド版 (chk.rb)

require 'net/http'

term = Time.now
domain = 'localhost'
path = '/sleep.php'
count = 10
rets = []
ts = []

count.times do |i|
ts << Thread.new(domain,"#{path}?code=#{i}") do |domain, path|
http = Net::HTTP.new(domain, 80)
req = Net::HTTP::Get.new(path)
rets << http.request(req).body
end
end

ts.each {|t|t.join}

p rets
printf("time: %1.3fsec\n", Time.now - term)
print 'RSS: ' + `ps alx | grep chk.rb | grep -v grep | awk '{print $8}'`

実行結果:

ruby -v
ruby 1.9.1p378 (2010-01-10 revision 26273) [i686-linux]
ruby ./chk.rb
["OK: code=3", "OK: code=0", "OK: code=2", "OK: code=4", "OK: code=9", "OK: code=5", "OK: code=6", "OK: code=1", "OK: code=8", "OK: code=7"]
time: 1.010sec
RSS: 6388

まとめ:


1. php curl_multi版 = 消費時間: 1.048秒 メモリ消費量: 11412KB+0KB = 11.1MB
2. php PCNTL版 = 消費時間: 1.149秒 メモリ消費量: 10752KB+51876KB=62628KB = 61.2MB
3. perl スレッド版 = 消費時間: 1.313秒 メモリ消費量: 31024KB = 30.3MB
4. ruby スレッド版 =消費時間: 1.010秒 メモリ消費量: 6388KB = 6.2MB

となりました。

1.のphp curl_multi版では子プロセスは使用されませんでした。やはり内部的にプロセス制御をおこなって並列処理を再現しているわけではありませんでした。
かといってcurlをバックグラウンドで並列処理させているのでは?といった検証はできていないので、推測の域を出ません。ただ、他の検証結果と比べても割と遜色ない結果が出ているので使い勝手はよさそうです。
でもphpはそもそもがHTMLの動的なレンダリングをサポートするために進化してきた言語なので、裏でもしかしたらトリッキーなことをしているのかも?という懸念もあります。curl_multiのソースを見てみたいですね。

代わりに、2. のphp PCNTL版ではちゃんと子プロセスを立てて、各々がHTTP通信をおこなうことで並列処理を実現していることが分かります。動きとしては見えやすく、素直ですが、しかしプロセスのメモリ消費量は61.2MBと、膨大です。並列処理をおこなう本数だけプロセスを立てるので当然ですが、多用したり並列処理の本数をあまり増やすのは怖いですね。

3. のperlはithreadという「スレッドにあってスレッドにあらず」というアプリケーションスレッドで並列処理を実現しているためでしょうか、実行時間もメモリ消費量も一番よくない成績となりました。perlでマルチスレッド、というのがどの程度現実的な手段として考えられているのか、によっては検証するまでもないのかもしれませんが。本当ならここでjavaが登場するべきですが、実行環境ができていないので(^^; またいずれ機会があったらにします。

4. のrubyはバージョン1.9以降のネイティヴスレッドの恩恵の現れでしょうか、いずれも一番よい成績です。逆にOSのリソースはどのくらい消費しているのか、見てみればよかったですが、これもおいおいということで。

ともあれ外部のAPIから通信結果を利用しているようなサービスから、ちょっと大きめなシステムで機能種別サーバ間でリクエストの受け渡しをしているようなサービスでもこのあたりの需要は多いと思うので、curl_multiは実行時間からみても、プロセスのメモリ消費量からみても、使える機能だなという感想です。

| | コメント (0) | トラックバック (0)

« 2010年6月 | トップページ | 2010年8月 »