基於PCNTL的PHP併發編程

原創文章,轉載請註明出處:http://huyanping.sinaapp.com/?p=178
作者:Jenner

PHP是一門較早出現的WEB開發腳本語言,並由於其語法結構簡單、易學、開源等特性迅速佔領WEB開發腳本語言領域,併成爲這個領域的龍頭老大直至今日。PHP從一出生就被設計用來快速開發WEB應用,這也註定了它在某些方面的先天不足,例如在cli環境下處理大量數據的情況,或者在併發編程方面,都顯得力不從心。

本文主要講解基於PCNTL的PHP併發編程,雖然PHP本身不支持多進程,但基於LINUX的PHP擴展PCNTL卻可以提供多進程編程。網絡上很多同類文章,但筆者進行多次嘗試後發現,不是難以控制進程數量,就是有潛在產生殭屍進程或孤兒進程的危險,或者父進程阻塞難以獲得更大的併發效果,且大多沒有介紹FORK的原理,使得PHP程序員學習PCNTL併發編程尤爲困難。本文力求解決這個問題。

FORK編程的大概原理是,每次調用fork函數,操作系統就會產生一個子進程,兒子進程所有的堆棧信息都是原封不動複製父進程的,而在fork之後,父進程與子進程實際上是相互獨立的,父子進程不會相互影響。也就是說,fork調用位置之前的所有變量,父進程和子進程是一樣的,但fork之後則取決於各自的動作,且數據也是獨立的;因爲數據已經完整的複製給了子進程。而唯一能夠區分父子進程的方法就是判斷fork的返回值。如果爲0,表示是子進程,如果爲正數,表示爲父進程,且該正數爲子進程的PID(進程號),而如果是-1,表示子進程創建失敗。請看如下代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$pid=pcntl_fork();
 
switch($pid){
case-1:
    echo"couldn't fork";
    break;
case0:
    echo"I'm parent";
    break;
default:
    echo"I'm child";
}
?>

以上代碼會產生一個子進程,並根據返回的pid進行父子進程的判斷。

生產環境中以上代碼大多是不適用的,我們需要大量的併發進程爲同時爲我們處理事情。這時,我們就需要fork多次,而產生的子進程數量需要在我們的控制之中,否則無限制的fork只會拖垮服務器。筆者曾經有過經歷,幾秒鐘服務器負載從0.3左右飆到800多,嚇的一身冷汗。

而子進程的使用通常會涉及到兩種:子進程執行完任務直接退出;子進程常駐內存,等待任務。以上兩種方式適用於不同情況。第一種情況大多我們不需要考慮太多,除非子進程的創建是循環進行的。而第二種則需要考慮進程間通信。

無論哪一種,無可避免的一個問題就是殭屍進程。殭屍進程就是子進程退出後,父進程沒有及時回收,系統仍然保留子進程的執行信息(例如PID,退出狀態等),留待其他程序讀取。如果殭屍進程數量很少,我們可以忽略掉。但如果是在一個循環中fork(併發編程中常見的死循環),這個問題就不能無視了,父進程必須定期回收已經退出的子進程。子進程的回收我們採用pcnt_wait函數來完成。如下面這段代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
while(true){
    $pid= pcntl_fork();
    switch($pid){
    case-1:
        echo"couldn't fork";
        break;
    case0:{
        $subPid= pcntl_wait($status);
        var_dump($status);
        break;
    }
    default:
        echo"I'm child";
        exit(0);
    }
}

以上代碼能夠循環產生子進程,並且父進程會阻塞等待子進程退出,這樣就產生了一個問題,父進程必須等待一個子進程退出後,再創建另外一個。額,這還是串行執行的不是嗎?是的,解決辦法就是將pcntl_wait函數替換成pcntl_waitpid()並添加WNOHANG參數。該函數可以在沒有子進程退出的情況下立刻跳出執行後續代碼。能夠達到更好的併發效果。具體代碼這裏不再演示。

循環創建子進程是一件非常浪費操作系統資源的事情。既然使用了死循環來處理任務,那麼就說明任務是一個可以隊列化的數據結構。我們可以採用進程間的通信,解決子進程退出重建的問題。而通信的機制主要有信號量、管道、共享內存等。然後我們需要一個生產者和消費者的模型。而基於fork的這種代碼編寫方式,非常不利於我們編寫複雜的業務邏輯。所以建議將進程控制與業務處理的代碼進程抽象隔離。進程間通信本文暫不涉及,如果讀者有需要可以閱讀關於管道、共享內存和信號量的文章。

根據上面所說,循環創建子進程會造成系統資源的浪費,而循環創建往往意味着任務可以隊列化。我們可以創建子進程後,讓子進程常駐內存,持續執行等待任務到達。而這類模型往往可以用生產-消費模型來實現。生產者負責將任務寫入隊列,而子進程從隊列中取出任務並執行。隊列的實現最好採用本身支持互斥的方式,這樣可以降低代碼的複雜度,管道是個不錯的選擇。

基於fork方式實現的多進程,由於我們只能使用Pid來做代碼隔離,所以進程控制中會充斥的各種if、else或者switch。這對實生產者和消費者模型造成一定難度。以下是一個生產者消費者的模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<?php
/**
 * @author:Jenner
 * @date 2014-01-14
 */
classJetMultiProcess {
 
    //最大隊列長度
    private$size;
 
    private$curSize;
 
    //生產者
    private$producer;
 
    //消費者
    private$worker;
 
    private$queueName;
 
    private$httpsqs;
 
    /**
     * 構造函數
     * @param string $worker 需要創建的消費者類名
     * @param int $size 最大子進程數量
     * @param $producer 需要創建的消費者類名
     */
    publicfunction __construct($producer$worker$size=10){
        $this->producer = new$producer;
        $this->worker = $worker;
        $this->size = $size;
        $this->curSize = 0;
    }
 
    publicfunction start(){
 
        $producerPid= pcntl_fork();
        if($producerPid== -1) {
            die("could not fork");
        elseif ($producerPid) {// parent
 
            while(true){
                $pid= pcntl_fork();
                if($pid== -1) {
                    die("could not fork");
                elseif ($pid) {// parent
 
                    $this->curSize++;
                    if($this->curSize>=$this->size){
                        $sunPid= pcntl_wait($status);
                    }
 
                else{// worker
 
                    $workernew$this->worker;
                    $worker->run();
                    exit();
                }
            }
 
        else{// producer
            $this->producer->run();
            exit();
        }
    }
}

以上代碼,通過size控制多進程數量,通過構造函數傳入生產者和消費者的類型。父進程第一次fork產生一個子進程生產者,然後再進行size次fork創建多個消費者。類似方法可以創建多個生產者和多個消費者協同工作。生產者和消費者都必須實現run方法,並在run方法中創建死循環。循環寫入和讀取隊列進行協同工作。該類沒有提供進程間通信的功能。通信需要在生產者和消費者類中實現。這樣能夠使得進程控制的代碼看起來更加簡潔。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章