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 ; case 0: 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 ; case 0:{ $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 */ class JetMultiProcess
{ //最大隊列長度 private $size ; private $curSize ; //生產者 private $producer ; //消費者 private $worker ; private $queueName ; private $httpsqs ; /** *
構造函數 *
@param string $worker 需要創建的消費者類名 *
@param int $size 最大子進程數量 *
@param $producer 需要創建的消費者類名 */ public function
__construct( $producer , $worker , $size =10){ $this ->producer
= new $producer ; $this ->worker
= $worker ; $this ->size
= $size ; $this ->curSize
= 0; } public function
start(){ $producerPid =
pcntl_fork(); if ( $producerPid ==
-1) { die ( "could
not fork" ); } else if
( $producerPid )
{ //
parent while (true){ $pid =
pcntl_fork(); if ( $pid ==
-1) { die ( "could
not fork" ); } else if
( $pid )
{ //
parent $this ->curSize++; if ( $this ->curSize>= $this ->size){ $sunPid =
pcntl_wait( $status ); } } else { //
worker $worker = new $this ->worker; $worker ->run(); exit (); } } } else { //
producer $this ->producer->run(); exit (); } } } |
以上代碼,通過size控制多進程數量,通過構造函數傳入生產者和消費者的類型。父進程第一次fork產生一個子進程生產者,然後再進行size次fork創建多個消費者。類似方法可以創建多個生產者和多個消費者協同工作。生產者和消費者都必須實現run方法,並在run方法中創建死循環。循環寫入和讀取隊列進行協同工作。該類沒有提供進程間通信的功能。通信需要在生產者和消費者類中實現。這樣能夠使得進程控制的代碼看起來更加簡潔。