Site icon Soul & Shell Blog

PHP 利用 ignore_user_abort + pcntl_fork 實作背景執行 (來來哥範例)

傳統的 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 可能就先抱歉了!

參考資料

Exit mobile version