PHPのpcntl_fork関数を使用すると、プロセスが分岐して親プロセスと子プロセスが並行して実行される。分岐点では両プロセスが同じコードを持ちながら、異なる実行パスを選択できる。
基本的な分岐動作
pcntl_forkの戻り値で役割を判定する:
-1:プロセス生成失敗0より大きい値:親プロセス(子プロセスのPID)0:子プロセス
以下のコードでは、if-elseの両方が実行されるという一見矛盾した結果が得られる。これはプロセスが複製され、それぞれが異なる条件分岐を通るためである。
<?php
$processId = pcntl_fork();
if ($processId === -1) {
exit('プロセス生成失敗');
} elseif ($processId > 0) {
// 親プロセスの処理
echo "親プロセス実行\n";
pcntl_wait($exitStatus);
} else {
// 子プロセスの処理
echo "子プロセス実行\n";
exit;
}
実行結果:
親プロセス実行 子プロセス実行
実行順序の非決定性
親子プロセスの実行順序はOSのスケジューリングに依存する。意図的に遅延を入れると、この特性が明確になる。
<?php
$processId = pcntl_fork();
if ($processId === -1) {
exit('プロセス生成失敗');
} elseif ($processId > 0) {
sleep(2);
echo "親プロセス実行\n";
pcntl_wait($exitStatus);
} else {
echo "子プロセス実行\n";
exit;
}
実行結果:
子プロセス実行 (2秒待機) 親プロセス実行
親プロセスにsleepを入れたにもかかわらず、子プロセスが先に出力される。両プロセスは独立した実行単位として動作する。
プロセス終了の待機機構
pcntl_waitは子プロセスの終了を待つブロッキング関数である。子プロセスが既に終了している場合は即座に復帰する。
<?php
$processId = pcntl_fork();
if ($processId === -1) {
exit('プロセス生成失敗');
} elseif ($processId > 0) {
pcntl_wait($exitStatus); // ここで待機
echo "親プロセス実行\n";
} else {
sleep(2);
echo "子プロセス実行\n";
exit;
}
実行結果:
子プロセス実行 (2秒待機後) 親プロセス実行
複数子プロセスの逐次待機
ループ内で逐次的に待機する場合、各子プロセスが順番に終了するまで親プロセスはブロックされる。
<?php
const WORKER_COUNT = 3;
$childProcesses = [];
for ($idx = 0; $idx < WORKER_COUNT; $idx++) {
$childProcesses[$idx] = pcntl_fork();
if ($childProcesses[$idx] === -1) {
exit('プロセス生成失敗');
} elseif ($childProcesses[$idx] > 0) {
// 親:即座に待機開始
pcntl_waitpid($childProcesses[$idx], $status);
echo "子プロセス{$idx}の終了を確認\n";
} else {
// 子:処理実行
sleep(1);
echo "ワーカー" . getmypid() . " 実行完了\n";
exit;
}
}
実行結果:
ワーカー58234 実行完了 子プロセス0の終了を確認 ワーカー58236 実行完了 子プロセス1の終了を確認 ワーカー58238 実行完了 子プロセス2の終了を確認
一括待機による並列実行
待機処理を分離することで、子プロセスは並列に実行される。
<?php
const WORKER_COUNT = 3;
$childProcesses = [];
for ($idx = 0; $idx < WORKER_COUNT; $idx++) {
$childProcesses[$idx] = pcntl_fork();
if ($childProcesses[$idx] === -1) {
exit('プロセス生成失敗');
} elseif ($childProcesses[$idx] === 0) {
sleep(1);
echo "ワーカー" . getmypid() . " 実行完了\n";
exit;
}
// 親はここで待機しない
}
// すべての子プロセスを一括で待機
foreach ($childProcesses as $pid) {
if ($pid > 0) {
pcntl_waitpid($pid, $status);
echo "親プロセス:終了確認\n";
}
}
実行結果:
ワーカー58279 実行完了 ワーカー58281 実行完了 ワーカー58280 実行完了 親プロセス:終了確認 親プロセス:終了確認 親プロセス:終了確認
子プロセスがほぼ同時に終了し、その後親プロセスが一括で復帰する。
プロセス間のデータ分離
子プロセスは親プロセスのメモリ空間を複製するが、書き込み時にコピーされる(COW: Copy-On-Write)。したがって、プロセス間で変数は共有されない。
<?php
const WORKER_COUNT = 3;
$childProcesses = [];
$fileHandle = fopen('./output.log', 'wb');
$counter = 1;
for ($idx = 0; $idx < WORKER_COUNT; $idx++) {
$childProcesses[$idx] = pcntl_fork();
if ($childProcesses[$idx] === -1) {
exit('プロセス生成失敗');
} elseif ($childProcesses[$idx] === 0) {
for ($loop = 0; $loop < 5; $loop++) {
flock($fileHandle, LOCK_EX);
fwrite($fileHandle, sprintf(
"%d : %s : %d\n",
getmypid(),
date('Y-m-d H:i:s'),
$counter
));
flock($fileHandle, LOCK_UN);
echo getmypid(), ": 書き込み成功\n";
$counter++;
}
exit;
}
}
foreach ($childProcesses as $pid) {
if ($pid > 0) {
pcntl_waitpid($pid, $status);
}
}
fclose($fileHandle);
ファイル出力結果:
58241 : 2018-02-02 15:55:48 : 1 58241 : 2018-02-02 15:55:48 : 2 58241 : 2018-02-02 15:55:48 : 3 58241 : 2018-02-02 15:55:48 : 4 58241 : 2018-02-02 15:55:48 : 5 58242 : 2018-02-02 15:55:48 : 1 58242 : 2018-02-02 15:55:48 : 2 58242 : 2018-02-02 15:55:48 : 3 58242 : 2018-02-02 15:55:48 : 4 58242 : 2018-02-02 15:55:48 : 5 58243 : 2018-02-02 15:55:48 : 1 58243 : 2018-02-02 15:55:48 : 2 58243 : 2018-02-02 15:55:48 : 3 58243 : 2018-02-02 15:55:48 : 4 58243 : 2018-02-02 15:55:48 : 5
各子プロセスの$counterは独立して1から開始される。これはメモリ空間が分離されていることを示す。ファイルアクセスにはflockによる排他制御が必要である。