傳統的 PHP 執行方式
傳統 PHP 執行 Script 都是在同一個 Thread,當我們利用 PHP 設計網頁或者 WebService 時,難免會遇到一些不需要當下回傳執行結果的案例,像是轉檔等背景執行之類的工作。由於這些工作可能會耗掉比較長的時間來運算,總不能為了要執行就讓使用者一直等下去。
利用 ignore_user_abort 忽略連線中斷
從上面的問題開始討論,假設有個轉檔的 PHP Script,執行太久造成連線中斷 (Timeout),一般來說 HTTP Server (通常是 Apache) 就會銷燬這個正在執行的 PHP Process!這下遭了,程式只跑了一半就 GG,圖轉了一半還好,如果是錢轉了一半可就...
為了解決這樣的問題,PHP 有個 ignore_user_abort(true); 函式可以忽略連線中斷而繼續執行,我們通常在使用上還會搭配 set_time_limit(0); 來關閉 PHP 的執行限制時間。這樣一來,無論使用者是否關閉瀏覽器,我們的程式皆可以在 Server 繼續執行。以下是「來來哥」模擬程式 PHP 範例 (沒聽過來來哥請自行 Youtube),程式會將來來哥說的話寫到 say.txt 檔案中,PHP Code 如下:
<?php date_default_timezone_set('Asia/Taipei'); // Ignore user aborts and allow the script to run forever ignore_user_abort(true); // disable php time limit set_time_limit(0); function job($jumperTime) { $startTime = time(); $handle = fopen('say.txt', 'a'); while ((time() - $startTime) < $jumperTime) { // Did the connection fail? if(connection_status() !== CONNECTION_NORMAL) { break; } // speak output $word = (rand(1, 3) === 1) ? '哩來!' : '來!'; fwrite($handle, sprintf("%s => ComeComeBrother: %s\n", date('H:i:s'), $word)); sleep(1); } fclose($handle); } echo date('H:i:s') . ' => Start!! '; // 模擬來來哥跳針持續 20 秒 job(20);
執行過程我們可以用 terminal 看一下執行結果 (Linux 使用 tail -f say.txt 即時顯示檔案內容),如下:
執行過程我們可以看到瀏覽器一直處於連線狀態!接下來我來利用 pcntl_fork 解決這個問題。
加上 pcntl_fork() 建立子執行緒
有了 ignore_user_abort(true); 只是表示 PHP 不會因為連線而被中斷執行,但是執行過程中這個 HTTP Request 仍然沒有結束,使用者的網頁就會一直處於等待回應的狀態。當然也可以用些奇怪的方法解決 (Dirty Solution),像是用 AJAX 呼叫、或者將 POST 導向一個隱藏的 iFrame 等等作法。但是遇到了 WebService 這樣鳥招可就不太優了,所以接下來介紹利用 PHP-pcntl Lib 中的 pcntl_fork() 命令將工作放在子執行緒執行,避免主執行緒等待執行結果。
典型的 pcntl_fork Sample Code Pattern 如下:
<?php $pid = pcntl_fork(); if ($pid === -1) { die('fork fail.'); } else if ($pid) { // main thread } else { // fork thread }
然後我們將原本要執行的工作放到 fork thread 中來執行,程式範例如下:
<?php date_default_timezone_set('Asia/Taipei'); // Ignore user aborts and allow the script to run forever ignore_user_abort(true); // disable php time limit set_time_limit(0); function job($jumperTime) { $startTime = time(); $handle = fopen('say.txt', 'a'); while ((time() - $startTime) < $jumperTime) { // Did the connection fail? if(connection_status() !== CONNECTION_NORMAL) { break; } // speak output $word = (rand(1, 3) === 1) ? '哩來!' : '來!'; fwrite($handle, sprintf("%s => ComeComeBrother: %s\n", date('H:i:s'), $word)); sleep(1); } fclose($handle); } echo date('H:i:s') . ' => Start!! '; // 模擬來來哥跳針持續 20 秒 if (function_exists('pcntl_fork')) { $pid = pcntl_fork(); if ($pid === -1) { die('fork fail.'); } else if ($pid) { // main thread // nothing to do } else { // fork thread job(20); } } else { job(20); }
執行的過程中我們可以看到瀏覽器馬上就結束連線,但是程式會繼續在 Server 背景執行,我們也可以多重新整理幾次,這樣就會有很多「來來哥」瘋狂的「來來來!」。
CentOS PHP 官方套件加裝 pcntl library 外掛
由於 CentOS 內建的 PHP 並沒有 pcntl 函式庫,在不重新編譯 PHP 的方式下可以透過 PHP extension 載入額外的函式。首先利用 phpinfo(); 查詢與確認 PHP 版本 (php -v 也行),然後到 http://www.php.net/releases/ 下載 Source Code 進行編譯,請透過以下方法僅編譯 pcntl extension 即可。
[root@linux ~]# cd php5-x.x.x/ext/pcntl [root@linux ~]# phpize [root@linux ~]# ./configure [root@linux ~]# make [root@linux ~]# cp modules/pcntl.so /usr/lib/php/modules/ [root@linux ~]# echo "extension=pcntl.so" > /etc/php.d/pcntl.ini
討論
雖然 ignore_user_abort(true); 好用,但是我們一但搭配了 set_time_limit(0); 來使用就表示這個 PHP 執行時間是沒有限制的,若是程式寫的不好或者執行佔用太多資源,很有可能會癱瘓系統運作,在設計時必須要謹慎使用!避免過多的執行緒癱瘓系統。此外 pcntl_fork 目前僅支援 Linux 環境,如果您是用 Windows 執行 PHP 可能就先抱歉了!