こんにちは!フルスタックエンジニアの先輩です。今回は、Hack言語の心臓部である「非同期処理(Awaitable)」と、それを支えるHHVM(HipHop Virtual Machine)のJITコンパイル構造について、深掘りしていきましょう。
他の言語(JavaScriptの`async/await`や、C#、Pythonなど)で非同期処理を触ったことがある方なら、「あぁ、あれね」と直感的に理解できるはずです。でも、HackとHHVMの組み合わせが叩き出すパフォーマンスの裏側には、他の言語とは一味違う、妥協のない最適化の哲学が隠されています。
ここをクリアすれば、Hackの並行処理の仕組みはバッチリマスターできますよ。それでは、エンジニアの頭の中を覗くような気持ちで、一緒に紐解いていきましょう!
—
1. Hackの非同期処理の基本:`Awaitable` って何だろう?
Webアプリケーションを作っていると、「外部APIを叩く」「データベースからデータを引いてくる」といった、待たされる処理(I/Oバウンドな処理)に直面しますよね。
普通の同期処理だと、レスポンスが返ってくるまでCPUが「暇な状態」になってしまいます。もったいないですよね。そこで登場するのが、Hackの `Awaitable
概念を図解してみよう
[リクエスト受信] ──> 処理Aを開始 ──> まだ終わらない?
│
▼ (ここで待たずに…)
処理Bを開始 ──> 同時に進行!
│
▼
両方揃ったら結合してレスポンス返却!
Hackでは、非同期関数を呼び出すと、すぐに実際の値ではなく `Awaitable`(「いつか結果を返すよ」という約束手形のようなオブジェクト) が返ってきます。
基本的な使い方を見てみよう
百聞は一見にしかず。実際にコードを見てみましょう。
hhfile
<<__EntryPoint>>
async function main_async(): Awaitable
// 2つの非同期処理の「約束手形」を受け取る(この時点ではまだ並行して走っている)
$user_task = fetch_user_data_async(1);
$orders_task = fetch_orders_async(1);
// どちらの処理も完了するのをここで「同時に」待つ
list($user, $orders) = await ConcurrentWaitAll::waitForAll(
tuple($user_task, $orders_task)
);
C\fprintf(STDOUT, “ユーザー名: %s, 注文数: %d\n”, $user, C\count($orders));
}
async function fetch_user_data_async(int $id): Awaitable
// 擬似的なI/O待ち(実際にはネットワーク通信など)
await Suspend::uSleep(100_000); // 100ms待つイメージ
return “Alice”;
}
async function fetch_orders_async(int $id): Awaitable
await Suspend::uSleep(100_000);
return vec[“Book”, “Coffee”];
}
ここで使っている `ConcurrentWaitAll::waitForAll()` がポイントです。別々に走らせたタスクを一つにまとめ、HHVMのイベントループに「この子たちが終わるまで効率よく待ってね」と指示を出しているんです。
—
2. HHVMとJITの裏側:Awaitableはどうスケジューリングされているのか?
さて、ここからが少しディープな話。HHVM(HipHop Virtual Machine)は、Hackコードを一度bytecodeにコンパイルし、さらに実行時にマシン語へとJIT(Just-In-Time)コンパイルします。
では、このJITコンパイルされた世界で、`await` や `Awaitable` はどのように扱われているのでしょうか?
イベントループとネイティブの融合
HHVMの非同期処理は、言語機能の皮を被った単なるライブラリではありません。HHVMのランタイムそのものにイベントループ(Reactor)が深く組み込まれています。
1. 状態のステートマシン化:
JITコンパイラは、`async` 関数を内部的に「ステートマシン(状態の遷移図)」へと変換してネイティブコードに最適化します。どこまで処理が進んだか(どの `await` で一時停止したか)を、CPUのレジスタや軽量なスタックフレーム上で超高速に管理します。
2. コンテキストスイッチの極小化:
OSのスレッドを何個も立てるのではなく、単一のスレッド(あるいは少数のプロセッサスレッド)上で、イベントループが `Awaitable` の完了状態を監視します。I/O待ちが発生すると、JITコードは即座に実行コンテキストを退避させ、別の実行可能な `Awaitable` にCPUコアを明け渡します。
3. プロファイリング駆動最適化(PGO)の恩恵:
JITは、頻繁に呼び出される非同期パスを検出し、ネイティブのマシン語レベルでインライン展開します。これにより、非同期処理特融の「オブジェクト生成やコールバックのオーバーヘッド」が極限まで削ぎ落とされているのです。
—
3. 陥りやすい文法エラーと罠
非同期処理を書き始めの頃に、誰もが一度はハマる「罠」があります。特にHackの厳格な型システムと組み合わせたときによくあるエラーを見ていきましょう。
罠1:`await` をつけ忘れる(型エラーの典型)
async function get_name_async(): Awaitable
return “Bob”;
}
async function bad_example_async(): Awaitable
// ❌ エラー! fetch_name_async() は Awaitable
// そのまま文字列として扱おうとしたり、awaitを書き忘れている
$name = get_name_async();
// 型チェッカーはこう怒ります:
// “Expected string, got Awaitable
}
対策:
関数が `Awaitable
罠2:不要な直列化(アンチパターン)
初心者がやりがちなのが、非同期なのに同期のように書いてしまうケースです。
// ❌ 悪い例(直列実行になってしまっている)
$a = await step1_async();
$b = await step2_async(); // step1が終わるまでstep2が始まらない!
これではせっかくのHHVMのイベントループの恩恵が台無しです。
// ⭕️ 良い例(並行実行)
$task_a = step1_async();
$task_b = step2_async();
list($a, $b) = await ConcurrentWaitAll::waitForAll(tuple($task_a, $task_b));
「まずタスクを発行して `Awaitable` を手に入れ、必要なところでまとめて `await` する」。このリズムを体に覚え込ませましょう。
—
まとめ
今回は、Hack言語の非同期処理とHHVMのJITコンパイル構造について解説しました。
- `Awaitable
` は、未来の結果を受け取るための約束手形。 - HHVMのランタイムとJITは、これをステートマシンとしてネイティブレベルで最適化し、イベントループ上で超効率的にスケジューリングしている。
- 型チェッカーのおかげで、非同期型のミスマッチはコンパイル時にすべて弾かれる。
Hackの静的型システムとJITの組み合わせがもたらすパフォーマンスは、大規模なWebサービスを支える強力な武器になります。この仕組みを味方につければ、あなたの書くコードはぐっと洗練され、スケーラブルなものになりますよ。
それでは、次のステップへ向けて、ハッピー・コーディング!