レベル: 中級 Cameron Laird (Cameron@Lairds.com), Vice President, Phaseit, Inc.
2007年 8月 07日 PHP 開発者の多くは、標準的な PHP にはスレッド機能がないため、実際の PHP アプリケーションでマルチタスク動作をすることはできないと考えています。例えば、アプリケーションが別の Web サイトの情報を必要とする場合、そのアプリケーションは、そのリモート情報の取得が完了するまで停止しなければならないと思ってしまうのです。しかし決してそんなことはありません。stream_select と stream_socket_client を利用した、PHP のプロセス内マルチタスク動作について学びましょう。
PHP はスレッド動作をサポートしていません。それにもかかわらず、また私が話をした大部分の PHP 開発者が信じていることとは大いに異なり、PHP アプリケーションはマルチタスク動作を行えるのです。まず、PHP プログラミングで「マルチタスク動作」と「スレッド動作」が何を意味するのかを、できるだけ明確にすることから始めましょう。
並行性の種類
最初に、中心のテーマと関連するいくつかの例を取り上げましょう。PHP は、マルチタスク動作、つまり並行性と複雑な関係を持っています。上位レベルでは、PHP は常にマルチタスク動作と関係しています。サーバー・サイドの PHP の標準的なシステム (例えば Apache のモジュールとしてのシステム) は、マルチタスク動作による方法で使われます。つまり、PHP が解釈する同じページをいくつかのクライアント (Web ブラウザー) が同時に要求し、そして Web サーバーはそれらのクライアントすべてに対して、ほとんど同時に返送します。
1 つの Web ページが別のページの送信を妨害することはありませんが、サーバーのメモリーやネットワーク帯域幅などの制限のあるリソースの部分で、ページ同士が少し干渉し合う可能性があります。このようなシステム全体としての並行性への要求に対しては、PHP ベースのソリューションが可能かもしれません。実装の言葉で言えば、PHP は、その PHP をコントロールする Web サーバーに並行性の責任を持たせるようにするのです。
ここ数年、Ajax の方法による、クライアント・サイドの並行性も開発者の関心の対象となりました。Ajax の意味は少し混乱してしまいましたが、Ajax の 1 つの側面として、ブラウザーの表示が、計算を行うと同時に、メニュー項目の選択などのユーザー・アクションに適切に応答を続けることができます。これは実際、一種のマルチタスク動作です。PHP でコーディングされた Ajax はマルチタスク動作を行いますが、そこに PHP が特に関係するわけではありません。他の言語用の Ajax フレームワークでも、PHP の場合とまったく同じ動作をします。
並行性の 3 番目の例として、PHP は表面的にしか関係しませんが、PHP/TK があります。PHP/TK は PHP の拡張であり、コアの PHP に対して、移植可能な GUI (graphical user interface) バインディングを提供します。PHP/TK を使うことで、PHP でコーディングしたデスクトップ GUI アプリケーションを作成することができます。PHP/TK の持つイベント・ベースの側面によって、学習しやすく、スレッド動作よりもエラーを起こしにくい形式の並行性をモデル化することができます。この場合も、並行性は PHP の基本的な機能ではなく、PHP を補完する技術から「継承」されるのです。
PHP そのものにスレッドのサポートを追加しようという、いくつかの試みも行われていますが、私の知る限り、どれも成功していません。しかし Ajax フレームワークと PHP/TK がイベント指向で実現したことは、PHP での並行性はスレッドよりもイベントを使った方が適切に表現できることを示しているのかもしれません。そして PHP V5 は、まさにその通りであることを実証しています。
PHP V5 には stream_select() が登場
標準的な PHP V4 とそれ以前では、PHP アプリケーションでのすべての動作はシーケンシャルに行う必要がありました。例えば、あるプログラムが商品の価格を 2 つの商用サイトで取得する必要がある場合、そのプログラムは、一方の価格を要求し、そのレスポンスが受信されるまで待ち、もう一方の価格を要求し、そして再度待つ必要がありました。
もしプログラムが、いくつかのタスクを同時に実行するように要求できるとしたらどうでしょう。その場合には、そのプログラムは全体として、シーケンシャルなプロセスで必要な時間の何分の一かの時間で終了できるはずです。
最初の例
新しい stream_select 関数と、この関数の仲間のいくつかの関数によって、いくつかのタスクを同時に実行することが可能になります。次の例を考えてみてください。
リスト 1. いくつかの HTTP ページを同時に要求する
<?php
echo "Program starts at ". date('h:i:s') . ".\n";
$timeout=10;
$result=array();
$sockets=array();
$convenient_read_block=8192;
/* Issue all requests simultaneously; there's no blocking. */
$delay=15;
$id=0;
while ($delay > 0) {
$s=stream_socket_client("phaseit.net:80", $errno,
$errstr, $timeout,
STREAM_CLIENT_ASYNC_CONNECT|STREAM_CLIENT_CONNECT);
if ($s) {
$sockets[$id++]=$s;
$http_message="GET /demonstration/delay?delay=" .
$delay . " HTTP/1.0\r\nHost: phaseit.net\r\n\r\n";
fwrite($s, $http_message);
} else {
echo "Stream " . $id . " failed to open correctly.";
}
$delay -= 3;
}
while (count($sockets)) {
$read=$sockets;
stream_select($read, $w=null, $e=null, $timeout);
if (count($read)) {
/* stream_select generally shuffles $read, so we need to
compute from which socket(s) we're reading. */
foreach ($read as $r) {
$id=array_search($r, $sockets);
$data=fread($r, $convenient_read_block);
/* A socket is readable either because it has
data to read, OR because it's at EOF. */
if (strlen($data) == 0) {
echo "Stream " . $id . " closes at " . date('h:i:s') . ".\n";
fclose($r);
unset($sockets[$id]);
} else {
$result[$id] .= $data;
}
}
} else {
/* A time-out means that *all* streams have failed
to receive a response. */
echo "Time-out!\n";
break;
}
}
?>
|
これを実行すると、次のような出力が表示されます。
リスト 2. リスト 1 のプログラムの典型的な出力
Program starts at 02:38:50.
Stream 4 closes at 02:38:53.
Stream 3 closes at 02:38:56.
Stream 2 closes at 02:38:59.
Stream 1 closes at 02:39:02.
Stream 0 closes at 02:39:05.
|
ここで何が起きているのかを理解することが重要です。上位レベルでは、この最初のプログラムは、いくつかの HTTP リクエストを行い、Web サーバーがこのプログラムに送信する何ページかを受信します。実動のアプリケーションでは、いくつかの Web サーバー (例えば google.com や yahoo.com、ask.com など) にリクエストを送信すると思いますが、この例では簡単にするために、アプリケーションのすべてのリクエストは Phaseit.net にある私達の会社のサーバーに送信されます。
要求された Web ページ群は、下記に示す状況によって変わる遅延時間の後、結果を返します。もしプログラムがシーケンシャルにリクエストを行ったとすると、終了するまでに約 15+12+9+6+3 (45) 秒かかることになります。しかし実際にはリスト 2 が示すように、15 秒で終了します。パフォーマンスが 3 倍に改善されるのは素晴らしいことです。
これを実現しているのが、PHP V5 の新しい stream_select 関数です。リクエストは従来の方法で開始されます。(いくつかの stream_socket_clients を開き、それぞれに対して http://phaseit.net/demonstration/delay?delay=$DELAY に対応する GET を作成します)。皆さん自身がこの URL をブラウザーからリクエストすると、数秒後に下記が表示されるはずです。
Starting at Thu Apr 12 15:05:01 UTC 2007.
Stopping at Thu Apr 12 15:05:05 UTC 2007.
4 second delay.
|
遅延サーバーは、次のように CGI で実装されています。
リスト 3. 遅延サーバーの実装
#!/bin/sh
echo "Content-type: text/html
<HTML> <HEAD></HEAD> <BODY>"
echo "Starting at `date`."
RR=`echo $REQUEST_URI | sed -e 's/.*?//'`
DELAY=`echo $RR | sed -e 's/delay=//'`
sleep $DELAY
echo "<br>Stopping at `date`."
echo "<br>$DELAY second delay.</body></html>"
|
リスト 3 に示す特定の実装は UNIX® 専用ですが、この記事のほとんどすべての内容は、Windows® (特に Windows 98 より新しいバージョン) または UNIX にインストールした PHP にも、ほとんどそのまま当てはまります。特にリスト 1 は、どちらのオペレーティング・システムでもホストすることができます。この記事の目的から見れば、Linux® も Mac OS X も UNIX のバリエーションであり、ここに示すコードはどちらのオペレーティング・システムでも動作します。
遅延サーバーへのリクエストは、次の順序で発行されます。
リスト 4. プロセスの起動シーケンス
delay=15
delay=12
delay= 9
delay= 6
delay= 3
|
stream_select の効果は、可能な限り早く結果を得られることです。この場合では、結果の受信順序は Web ページの発行順序とは逆です。3 秒後に、最初のページが読み取り可能になります。プログラムのこの部分も、従来の PHP (この場合は fread ) です。他の PHP プログラムと同様、この読み取りは fgets を使っても同じように行うことができます。
この同じ方法で処理が続けられます。データが準備できるまで、プログラムは stream_select で停止します。重要なことは、いずれかの接続にデータがありさえすれば、データの順序はどうであってもプログラムがデータの読み取りを開始する点です。プログラムはこのようにして、マルチタスク動作、つまりいくつかのリクエストの結果の並行処理を行います。
これがホスト CPU にとって何ら負荷とならないことに注意してください。while の中の fread をネットワーク・プログラム全体で実行することで CPU 使用率が 100 パーセントになってしまうことは珍しくありません。しかしこの場合には、そうはなりません。なぜなら stream_select は、読み取りが可能なときには即座に反応する一方、読み取りと読み取りの間に待つ間にはほとんど CPU 負荷をかけない、という望ましい特性を持っているのです。
stream_select() について知っておくべきこと
このような、イベント・ベースのプログラミングは初歩的なものではありません。リスト 1 は必要最小限のコードのみに絞ってありますが、マルチタスク動作のアプリケーションで必要となるコールバックや調整を含むコードの作成は、単純な手続き型のシーケンスよりもなじみが薄いものかもしれません。この場合、困難な部分の中心は、$read 配列にあります。これが参照であることに注目してください。つまり stream_select は、$read の内容を変更することで重要な情報を返します。C で最もつまずきやすい部分がポインターと言われているのと同様に、プログラマーにとって参照は PHP で最も困難な部分のようです。
この方法を使って任意の数の外部 Web サイトにリクエストを行うと、プログラムはそれぞれの結果を可能な限り早く受信し、他のリクエストに対する結果を待ちません。実際、これと同じ方法を使うことで、Web ポート 80 への接続だけではなく任意の TCP/IP 接続を適切に処理することができます。つまり原理的には、LDAP の取得や SMTP 送信、SOAP リクエストなども処理できるのです。
しかし、それだけではありません。PHP V5 はさまざまな接続を、単なるソケットではなく「ストリーム」として扱います。PHP の CURL (Client URL library) は、HTTPS 証明書や FTP アップロード、クッキーその他をサポートしています。(CURL を利用することで、PHP アプリケーションはさまざまなプロトコルを使ってサーバーに接続することができます。) CURL が提供するのはストリーム・インターフェースであるため、プログラムから見ると接続は透過的です。次のセクションでは、stream_select によってローカルの計算まで多重化できる方法を示します。
stream_select には、いくつか注意すべき点もあります。ドキュメントが整備されておらず、最新の PHP の本にも stream_select は説明されていません。Web 上で入手できるいくつかのコード例は、動作しないか、あるいはわかりにくいものです。stream_select の 2 番目と 3 番目の引数 (リスト 1 の read チャネルに対応する write チャネルと exception チャネルを管理します) は、ほとんど必ずと言っていいほどヌルでなければなりません。ごくわずかの例外を除き、書き込み可能なチャネルあるいは例外チャネルを選択することは誤りであり、十分な経験を積むまで読み取り可能のみを選択すべきです。
また、少なくとも PHP V5.1.2 という新しいバージョンでさえ、stream_select には明らかな誤りがあります。最も重大な誤りは、この関数の戻り値を信頼できないことです。私はまだこの関数の実装をデバッグしていませんが、私の経験では、(リスト 1 のように) count($read) はテストをしても問題ありませんが、(正式なドキュメンテーションに書かれていることとは異なり) stream_select 自体の戻り値のテストはうまく行きません。
ローカルでの PHP の並行性
先ほど挙げた例と、これまでの説明の大部分は、いくつかのリモート・リソースを同時に処理する方法と、(オリジナルのリクエストの順序に従って各結果の処理を待つのではなく) 到着する順に結果を受信する方法に焦点を当てました。これは確かに PHP の並行性の重要な使い方です。実際のアプリケーションでは、場合によると 10 倍以上の高速化を実現することができます。
もし、速度の低下がローカル動作によるものだとしたらどうでしょう。ローカル処理によって制限されている、PHP の結果が得られるまでの時間を短くする方法はあるのでしょうか。いくつかの方法がありますが、これらの方法は、リスト 1 のソケットによる方法よりも、さらに知られていないかもしれません。その理由としては、次のようなものが挙げられます。
- 大部分の PHP ページは十分に高速です。より高いパフォーマンスは利点ではありますが、新しいコードを作成するのに見合うほどの利点ではありません。
- Web ページでの PHP の使い方では、部分的な高速化は重要ではありません。計算順序を変更することで中間結果を早く利用できるようになったとしても、評価の基準が Web ページ全体を送信するためにかかる時間のみの場合には関係がありません。
- ローカルのボトルネックになっているものの中で PHP がコントロールできるものはほとんどありません。例えば、ユーザーは口座の記録から詳細な情報を得るのに 8 秒かかると文句を言うかもしれませんが、それはおそらく、PHP とは無関係なデータベース処理や他のリソースの処理の制約によるものです。たとえ PHP による処理をゼロにできたとしても、情報の検索を行うだけで相変わらず 7 秒以上かかるでしょう。
- 並列化できる制約は、それよりもさらにわずかです。例えば、あるページが特定の上場普通株の希望取引価格を計算するとし、計算はかなり複雑で何秒もかかるとしましょう。この計算は本質的にシーケンシャルに行われるものかもしれません。この計算を「チームワーク」を発揮できるように分割できる簡単な方法はありません。
- 並行性に関して PHP が持つ潜在的な力を理解している PHP プログラマーは、ほとんどいません。並列化を行えるパフォーマンス要件を抱える少数のプログラマーのうち、私が出会った人の大部分は「PHP ではスレッド動作ができない」と繰り返し、彼らが従来から使っている既存の計算モデルに頼ろうとするのです。
しかし場合によると、PHP によってパフォーマンスを向上できることがあります。例えば、2 つの株価を計算し、場合によっては両者の比較を行う必要があるPHP のページがあるとしましょう。そのページのホストがマルチプロセッサーである場合には、時間のかかる 2 つの異なる計算を別々のプロセッサーに割り当てることによって、パフォーマンスをほとんど 2 倍にできる可能性があります。
PHP での計算という世界全体から見ると、そうした例は稀です。しかし私はこれを正確に説明したものを他で見たことがないので、そうした高速化のモデルをここで紹介することにしました。
リスト 5. 遅延サーバーの実装
<?php
echo "Program starts at ". date('h:i:s') . ".\n";
$timeout=10;
$streams=array();
$handles=array();
/* First launch a program with a delay of three seconds, then
one which returns after only one second. */
$delay=3;
for ($id=0; $id <= 1; $id++) {
$error_log="/tmp/error" . $id . ".txt"
$descriptorspec=array(
0 => array("pipe", "r"),
1 => array("pipe", "w"),
2 => array("file", $error_log, "w")
);
$cmd='sleep ' . $delay . '; echo "Finished with delay of ' .
$delay . '".';
$handles[$id]=proc_open($cmd, $descriptorspec, $pipes);
$streams[$id]=$pipes[1];
$all_pipes[$id]=$pipes;
$delay -= 2;
}
while (count($streams)) {
$read=$streams;
stream_select($read, $w=null, $e=null, $timeout);
foreach ($read as $r) {
$id=array_search($r, $streams);
echo stream_get_contents($all_pipes[$id][1]);
if (feof($r)) {
fclose($all_pipes[$id][0]);
fclose($all_pipes[$id][1]);
$return_value=proc_close($handles[$id]);
unset($streams[$id]);
}
}
}
?>
|
このプログラムを実行すると以下の出力が生成されます。
Program starts at 10:28:41.
Finished with delay of 1.
Finished with delay of 3.
|
ここでのポイントは、PHP が 2 つの独立したサブプロセスを起動し、2 番目のサブプロセスの方が先に開始しているにもかかわらず最初のサブプロセスの出力を取得して終了させ、次に 2 番目のサブプロセスを取得している点です。もしホストがマルチプロセッサーのマシンだったとし、またオペレーティング・システムが適切に構成されているとすると、別のプロセッサーに別のサブプログラムを割り当てる作業をオペレーティング・システム自体が行います。これは PHP を使ってマルチプロセッサーのホストを活用する 1 つの方法です。
まとめ
PHP はマルチタスク動作を行うことができます。PHP は Java™ プログラミング言語や C++ などが行う方法と同じ方法でのスレッド動作はサポートしていませんが、上記の例は、多くの人が認識している以上に PHP で高速化を実現できる可能性があることを示しています。
参考文献 学ぶために
製品や技術を入手するために
- 皆さんの次期オープンソース開発プロジェクトを IBM trial software を使って革新してください。ダウンロード、あるいは DVD で入手することができます。
-
IBM 製品の試用版をダウンロードし、DB2® や Lotus®、Rational®、Tivoli®、WebSphere® などが提供するアプリケーション開発ツールやミドルウェア製品をお試しください。
議論するために
著者について  | 
|  | Cameronは、Phaseit, Inc. の常勤のコンサルタントです。オープン・ソースなどの技術的なトピックについて、数々の執筆や発言を行っています。Cameronのメール・アドレスはclaird@phaseit.net です。 |
記事の評価
|