逐次実行という「罪」:HH\Asioで書き換える、次世代の並列I/O戦略
PHPの歴史において、我々は長らく「同期的な逐次処理」という名の呪縛に縛られてきた。`foreach`の中で平然と発行されるAPIコールやDBクエリ。それらが積み重なり、レスポンスタイムが線形に悪化していく様子を、ただ眺めるしかなかった時代は終わった。
Hackの真髄は、型安全性だけではない。HHVMの心臓部であるAsio(Async I/O)をどう使いこなすか。これが、君が書くコードが「ただ動くもの」か、それとも「高負荷に耐えうる芸術品」かを分かつ境界線だ。
今回は、実務で最も陥りやすい「逐次実行の罠」を叩き割り、`HH\Asio\v`と`HH\Asio\m`を駆使した並列処理の最適解を伝授する。
—
1. 逐次ループというボトルネック
まずは、レビューで即座に却下される「悪しきコード」を見てほしい。
// アンチパターン:レイテンシをドブに捨てている
async function get_user_data_sequential(vec
$results = vec[];
foreach ($ids as $id) {
// 次のループは、前のAPIコールが終わるまで開始されない
$results[] = await fetch_api_user_data($id);
}
return $results;
}
このコードのコストは `O(N Latency)` だ。もしAPIのレイテンシが100msで、10個のユーザーIDを処理すれば、合計1秒の無駄な待機が発生する。HHVMは非同期I/Oをサポートしているにもかかわらず、これでは宝の持ち腐れだ。
—
2. HH\Asio\v:並列実行の「銀の弾丸」
複数の独立した非同期タスクがある場合、`HH\Asio\v`(Vector)こそが最適解だ。これは、与えられた`Awaitable`のベクターを同時に開始し、すべてが完了するまで待機する。
use HH\Asio;
async function get_user_data_parallel(vec
// Awaitableの配列を作成し、即座に非同期実行を開始する
$tasks = vec[];
foreach ($ids as $id) {
$tasks[] = fetch_api_user_data($id);
}
// HH\Asio\vで一斉に待機。コストは O(Max(Latency)) に劇的に減少する
return await Asio\v($tasks);
}
ここが設計の肝だ: `await`をループの中に置くな。`await`は「そのタスクの結果が戻るまで待機する」という停止点だ。ループの外で`Awaitable`を生成し、一気に`Asio\v`へ渡す。これだけで、CPUはI/O待ちの間に次のリクエストを発行できるようになる。
—
3. HH\Asio\m:マップを扱うための洗練されたインターフェース
実際の開発では、単なるリストではなく、キーと値が紐付いた構造(`Map`や`dict`)を扱うことが多いだろう。ここで`HH\Asio\m`(Map)の出番だ。
async function fetch_bulk_data(vec
$tasks = dict[];
foreach ($ids as $id) {
$tasks[$id] = fetch_api_user_data($id);
}
// キーを維持したまま、並列処理の結果を返却
return await Asio\m($tasks);
}
`Asio\m`の優れている点は、戻り値の型が元のdictのキーと一致することだ。型システムがこの整合性を保証しているため、実行後にキーの対応関係で頭を悩ませる必要はない。
—
4. プロダクション環境での注意点:落とし穴を回避せよ
どれほど強力なツールも、設計を誤ればシステムを破壊する。
1. 接続数制限(Throttling)の意識
1,000個のIDに対して`Asio\v`を投げるな。外部APIのレートリミットやコネクションプールを枯渇させ、自らDDOS攻撃を仕掛けることになる。
- 対策: `chunk`を用いて一定数ずつ並列化せよ。
async function safe_bulk_fetch(vec
$results = vec[];
foreach (array_chunk($ids, 20) as $chunk) {
$tasks = vec[];
foreach ($chunk as $id) $tasks[] = fetch_api_user_data($id);
$results = Vec\concat($results, await Asio\v($tasks));
}
return $results;
}
2. 例外処理の複雑性
`Asio\v`や`Asio\m`の中で1つでも例外が発生すると、他の成功したタスクの結果はどうなるのか?
- 知見: `Asio\v`は、いずれかのタスクが失敗すれば即座に例外をスローする。もし部分的な成功を許容したいなら、各タスクを`Result`型でラップするか、`try-catch`で個別にハンドリングした`Awaitable`を構築する設計が必要だ。
—
最後に:コードは「意図」を語るべきだ
PHPからHackへ移行するということは、単に型を書くことではない。「並列性がどこに存在し、どこで同期すべきか」という、システム全体のフローを設計し直すことだ。
`HH\Asio\v`と`HH\Asio\m`は、君のコードから冗長な待ち時間を削ぎ落とし、マシンのリソースを最大限に引き出すための強力なナイフだ。しかし、ナイフをどう使うかは設計者である君次第である。
さあ、古いループ処理を書き換えろ。レイテンシを殺し、パフォーマンスの限界を超えていけ。それがHackのコアコミッターたる私からの要求だ。