PHP 本身不支援原生多線程,但可透過 Web 伺服器的多線程特性或 PCNTL 擴展實現並行處理。本文詳細介紹兩種主要方案:偽多線程(借助 Web Server)與真正的多進程(PCNTL/popen)。
原理:Web 伺服器(如 Apache/Nginx)本身支援多線程,我們可發起多個 HTTP 請求來同時執行不同 PHP 腳本或同一腳本的不同邏輯。
<?php
function runThread() {
$fp = fsockopen('localhost', 80, $errno, $errmsg);
fputs($fp, "GET /a.php?act=b\r\n\r\n");
fclose($fp);
}
function a() {
$fp = fopen('result_a.log', 'w');
fputs($fp, 'Set in ' . date('h:i:s', time()) . (double)microtime() . "\r\n");
fclose($fp);
}
function b() {
$fp = fopen('result_b.log', 'w');
fputs($fp, 'Set in ' . date('h:i:s', time()) . (double)microtime() . "\r\n");
fclose($fp);
}
if (!isset($_GET['act'])) { $_GET['act'] = 'a'; }
if ($_GET['act'] == 'a') {
runThread(); // 發起另一個請求執行 b()
a();
} else if ($_GET['act'] == 'b') {
b();
}
?>
比較 result_a.log 與 result_b.log 的時間戳,可驗證兩者確實並行執行。
flock() 或建立臨時鎖檔案(while(file_exists('lock'));)。runThread 中讀取回應,必須使用 socket_set_nonblock(),否則會阻塞導致偽多線程失效。大量網路請求爬蟲(如同時抓取多個電商網站商品資訊),可顯著消除等待回應的瓶頸。
需編譯 PHP 時加入 --enable-pcntl,建議僅在 CLI 模式下使用,不要在 Web 伺服器環境運行。
declare(ticks=1);
$bWaitFlag = FALSE;
$intNum = 10;
$pids = array();
echo "Start\n";
for ($i = 0; $i < $intNum; $i++) {
$pids[$i] = pcntl_fork();
if (!$pids[$i]) {
// 子進程代碼
$str = "";
sleep(5 + $i);
for ($j = 0; $j < $i; $j++) { $str .= "*"; }
echo "$i -> " . time() . " $str\n";
exit();
}
}
if ($bWaitFlag) {
for ($i = 0; $i < $intNum; $i++) {
pcntl_waitpid($pids[$i], $status, WUNTRACED);
echo "wait $i -> " . time() . "\n";
}
}
echo "End\n";
執行結果差異:
$bWaitFlag = FALSE:父進程不等待,直接印出 End,子進程在背景並行執行。$bWaitFlag = TRUE:父進程逐個等待子進程結束,輸出交錯顯示 wait 資訊。pcntl_fork() 後,子進程從該行繼續執行,並複製父進程資料(Copy-on-Write),透過 if (!$pids[$i]) 區分父子進程邏輯。
利用 popen() 啟動子進程並讀取其輸出,適用於無 PCNTL 環境。
$handle1 = popen('php sub.php php1', 'r');
$handle2 = popen('php sub.php php2', 'r');
$handle3 = popen('php sub.php php3', 'r');
while (!feof($handle1) || !feof($handle2) || !feof($handle3)) {
$read = fgets($handle1); echo $read;
$read = fgets($handle2); echo $read;
$read = fgets($handle3); echo $read;
}
pclose($handle1); pclose($handle2); pclose($handle3);
輸出時間戳顯示三個子進程確實並發執行。
$exec_number = 40;
$tasklist = file($argv[1]);
$tasklist_len = count($tasklist);
$tasklist_pos = 0;
$handle_list = array();
while (1) {
// 補滿空閒槽位
if ($exec_number > count($handle_list) && $tasklist_pos < $tasklist_len) {
for ($i = $tasklist_pos; $i < $tasklist_len; ) {
$command = $tasklist[$i];
$handle_list[] = popen($command, "r");
$i++;
if ($exec_number == count($handle_list)) break;
}
$tasklist_pos = $i;
}
if (0 == count($handle_list)) break;
// 檢查子進程輸出,清理已結束進程
$end_handle_keys = array();
foreach ($handle_list as $key => $handle) {
$str = fread($handle, 65536);
echo $str;
if (feof($handle)) {
$end_handle_keys[] = $key;
pclose($handle);
}
}
foreach ($end_handle_keys as $key) {
unset($handle_list[$key]);
}
}
do {
if (($msgsock = socket_accept($sock)) < 0) {
echo "socket_accept() failed: " . socket_strerror($msgsock) . "\n";
break;
}
$pid = pcntl_fork();
if ($pid == -1) { die('could not fork'); }
else if (!$pid) {
// 子進程處理連線
socket_write($msgsock, $msg, strlen($msg));
do { /* ... */ } while (true);
socket_close($msgsock);
}
} while (true);
根據專案環境與需求選擇合適方案,可大幅提升 PHP 應用的並發處理能力。