首頁 >後端開發 >php教程 >初步進行PHP多進程程式設計的介紹

初步進行PHP多進程程式設計的介紹

不言
不言原創
2018-07-05 10:37:061253瀏覽

這篇文章主要介紹了關於初步進行PHP多進程編程的介紹,有著一定的參考價值,現在分享給大家,有需要的朋友可以參考一下

羨慕火影忍者裡鳴人的影分身麼?沒錯,PHP程式是可以開動影分身的!想完成任務,又覺得一個進程太慢,那麼,試試看用多進程來搞吧。這篇文章將會介紹PHP多進程的基本需求,如何建立多進程以及基本的訊號控制,暫時不會告訴你如何進行進程間通訊和資訊共享。

1. 準備

在動手之前,請確定你用的不是M$ Windows平台(因為我沒有Windows)。 Linux / BSD / Unix應該都是沒問題的。確認好了工作環境以後一起來看看我們需要的PHP模組是否都有。開啟終端機輸入下面的指令:

$ php -m

這個指令檢查並列印目前PHP所有開啟的擴展,看看pcntlposix是否在輸出的清單中。

1.1. pcntl

如果找不到pcntl,八成是編譯的時候沒把這個擴充編譯進去。如果你跟我一樣是編譯安裝的PHP,那就需要重新編譯安裝PHP。在配置的時候記得加上--enable-pcntl參數即可。

$ cd /path/to/php_source_code_dir 
$ ./configure [some other options] --enable-pcntl
$ make && make install

1.2. posix

這貨一般預設就會裝上,只要你編譯的時候沒有加上--disable-posix

2. 預備知識

在繼續之前,你還需要對Linux多進程有一點了解。多進程是咋回事呢?這裡可跟火影忍者裡的影分身稍微有點不一樣。首先,鳴人從小長到大,例如16歲,咳。有一天他發動了影分身,分出了5個他。顯然,這些分身也是16歲的鳴人而不是剛出生啥也不懂就會哭的嬰兒(那叫克隆)。然後,不一樣的地方來了:分身們變成了獨立的人各自去做各自的事,互相之間不再知道其他分身和原身都做了什麼(當然不會像動畫片裡一樣積累經驗給原身啦)。除非,他們彼此之間有交流,不然,只有16歲之前的事情才是他們共同的記憶。

有同學說了,老大你這不坑爹呢?我又沒看過火影忍者!那你去看一遍好了…

最後,預備知識完了,就是大致了解一下主進程開出來的子進程是怎麼回事。子進程的程式碼和主進程是完全一樣的,還有一部分一樣的東西就是直到發動影分身之前執行的所有內容。詳情請參閱《操作系統》課程。

3. 影分身之術

所以呢,沒有點基礎知識怎麼能理解捲軸裡的內容呢?打開捲軸首先看到了一個單字:fork。

3.1. fork

叉子?叉子是分岔的,一個變多個嘛!差不多就是這個意思。創建子進程就用這個命令。這裡需要用到pcntl_fork()函數。 (可以先簡單看一下PHP手冊關於這個函數的介紹。)建立一個PHP腳本:

$pid = pcntl_fork(); // 一旦调用成功,事情就变得有些不同了
if ($pid == -1) {    
die('fork failed');
} else if ($pid == 0) {} else {}

pcntl_fork()函數建立一個子進程,子進程和父進程唯一的差異就是PID(進程ID)和PPID(父進程ID)不同。在終端機下查看進程用ps指令(問man看ps怎麼用:man ps)。當函數傳回值為-1的時候,表示fork失敗了。試試在​​if前面加一句:echo $pid . PHP_EOL;。運行你的腳本,輸出可能像下面這樣(結果說明子進程和父進程的程式碼是相同的):

67789 # 这个是父进程打印的
0     # 这个是子进程打印的

pcntl_fork()函數呼叫成功後,在父進程中會傳回子進程的PID,而在子進程中回傳的是0。所以,下面直接用if分支來控制父行程和子行程做不同的事。

3.2. 分配任務

然後我們來說說鳴人16歲那次影分身的事兒,給原身和分身分配兩個簡單的輸出任務:

$parentPid = getmypid(); // 这就是传说中16岁之前的记忆
$pid = pcntl_fork(); // 一旦调用成功,事情就变得有些不同了
if ($pid == -1) {    
die('fork failed');
} else if ($pid == 0) {    
$mypid = getmypid(); // 用getmypid()函数获取当前进程的PID    
echo 'I am child process. My PID is ' . $mypid . ' and my father's PID is ' . $parentPid . PHP_EOL;
} else {    
echo 'Oh my god! I am a father now! My child's PID is ' . $pid . ' and mine is ' . $parentPid . PHP_EOL;}

輸出的結果可能是這樣:

Oh my god! I am a father now! My child's PID is 68066 and mine is 68065
I am child process. My PID is 68066 and my father's PID is 68065

再強調一下,pcntl_fork()呼叫成功以後,一個程式變成了兩個程式:一個程式得到的 $pid變數值是0,它是子行程;另一個程式得到的$pid的值大於0,這個值是子行程的PID,它是父行程。在下面的分支語句中,由於$pid值的不同,運行了不同的程式碼。再次強調:子進程的程式碼和父進程的是一樣的。所以就要透過分支語句來分配不同的任務給他們。

3.3. 子进程回收

刚刚有man ps么?一般我习惯用ps aux加上grep命令来查找运行着的后台进程。其中有一列STAT,标识了每个进程的运行状态。这里,我们关注状态Z:僵尸(Zombie)。当子进程比父进程先退出,而父进程没对其做任何处理的时候,子进程将会变成僵尸进程。Oops,又跟火影里的影分身不一样了。鸣人的影分身被干死了以后就自动消失了,但是这里的子进程分身死了话还留着一个空壳在,直到父进程回收它。僵尸进程虽然不占什么内存,但是很碍眼,院子里一堆躺着的僵尸怎么都觉得怪怪的。(别忘了它们还占用着PID)

一般来说,在父进程结束之前回收挂掉的子进程就可以了。在pcntl扩展里面有一个pcntl_wait()函数,它会将父进程挂起,直到有一个子进程退出为止。如果有一个子进程变成了僵尸的话,它会立即返回。所有的子进程都要回收,所以多等等也没关系啦!

3.4. 父进程先挂了

如果父进程先挂了怎么办?会发生什么?什么也不会发生,子进程依旧还在运行。但是这个时候,子进程会被交给1号进程,1号进程成为了这些子进程的继父。1号进程会很好地处理这些进程的资源,当它们结束时1号进程会自动回收资源。所以,另一种处理僵尸进程的临时办法是关闭它们的父进程。

4. 信号

一般多进程的事儿讲到上面就完了,可是信号在系统中确实是一个非常重要的东西。信号就是信号灯,点亮一个信号灯,程序就会做出反应。这个你一定用过,比如说在终端下运行某个程序,等了半天也没什么反应,可能你会按 Ctrl+C 来关闭这个程序。实际上,这里就是通过键盘向程序发送了一个中断的信号:SIGINT。有时候进程失去响应了还会执行kill [PID]命令,未加任何其他参数的话,程序会接收到一个SIGTERM信号。程序收到上面两个信号的时候,默认都会结束执行,那么是否有可能改变这种默认行为呢?必须能啊!

4.1. 注册信号

人是活的程序也是活的,只不过程序需要遵循人制定的规则来运行。现在开始给信号重新设定规则,这里用到的函数是pcntl_signal()(继续之前为啥不先查查PHP手册呢?)。下面这段程序将给SIGINT重新定义行为,注意看好:

// 定义一个处理器,接收到SIGINT信号后只输出一行信息
function signalHandler($signal) {    
if ($signal == SIGINT) {        
echo 'signal received' . PHP_EOL;    }}
// 信号注册:当接收到SIGINT信号时,调用signalHandler()函数pcntl_signal(SIGINT, 'signalHandler');while (true) {    sleep(1);    // do something    pcntl_signal_dispatch(); // 接收到信号时,调用注册的signalHandler()}

执行一下,随时按下 Ctrl+C 看看会发生什么事。

4.2. 信号分发

说明一下:pcntl_signal()函数仅仅是注册信号和它的处理方法,真正接收到信号并调用其处理方法的是pcntl_signal_dispatch()函数。试试把// do something替换成下面这段代码:

for ($i = 0; $i < 1000000; $i++) {    
echo $i . PHP_EOL;    
usleep(100000);}

在终端下执行这个脚本,当它不停输出数字的时候尝试按下 Ctrl+C 。看看程序有什么响应?嗯……什么都没有,除了屏幕可能多了个^C以外,程序一直在不停地输出数字。因为程序一直没有执行到pcntl_signal_dispatch(),所以就并没有调用signalHandler(),所以就没有输出signal received

4.3. 版本问题

如果认真看了PHP文档,会发现pcntl_signal_dispatch()这个函数是PHP 5.3以上才支持的,如果你的PHP版本大于5.3,建议使用这个方法调用信号处理器。5.3以下的版本需要在注册信号之前加一句:declare(ticks = 1);表示每执行一条低级指令,就检查一次信号,如果检测到注册的信号,就调用其信号处理器。想想就挺不爽的,干嘛一直都检查?还是在我们指定的地方检查一下就好。

4.4. 感受僵尸进程

现在我们回到子进程回收的问题上(差点忘了= =")。当你的一个子进程挂了(或者说是结束了),但是父进程还在运行中并且可能很长一段时间不会退出。一个僵尸进程从此站起来了!这时,保护伞公司(内核)发现它的地盘里出现了一个僵尸,这个僵尸是谁儿子呢?看一下PPID就知道了。然后,内核给PPID这个进程(也就是僵尸进程的父进程)发送一个信号:SIGCHLD。然后,你知道怎么在父进程中回收这个子进程了么?提示一下,用pcntl_wait()函数。

4.5. 發送訊號

希望剛剛有認真man過kill指令。它其實就是向行程發送訊號,在PHP中也可以呼叫posix_kill()函數來達到相同的效果。有了它就可以在父進程中控制其他子進程的運行了。例如在父進程結束之前關閉所有子進程,那麼fork的時候在父進程記錄所有子進程的PID,父進程結束之前依序給子進程發送結束訊號即可。

5. 實踐

PHP的多進程跟C還是挺像的,搞明白了以後用其他語言寫的話也大同小異差不多都是這麼個情況。如果有空的話,試著寫一個小程序,切身體會一下個中滋味:

  1. #16歲的鳴人發送影分身,分出5個分身

  2. 每個分身隨機生存10到30秒,每秒都輸出點什麼

  3. #保證原身能感受到分身的結束,然後開動另一個分身,保證最多有5個分身

  4. 不使用nohup,讓原身在終端關閉後依舊能夠運作

  5. 把分身數量(5)寫進一個設定檔裡,當給原身發送訊號(可以考慮用SIGUSR1或SIGUSR2)時,原身讀取設定檔並更新允許的分身最大數量

  6. #如果分身多了,關閉幾個;如果少了,再分出來幾個

#提示:

  1. while循環保證進程運行,注意sleep以免100%的CPU佔用

  2. 運行進程的終端被關閉時,程式會收到一個SIGHUP訊號

  3. 可以用parse_ini_file()函數解析INI設定檔

##以上就是本文的全部內容,希望對大家的學習有幫助,更多相關內容請關注PHP中文網!

相關推薦:

關於PHP多態性的理解

PHP檔案程式設計的介紹

以上是初步進行PHP多進程程式設計的介紹的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn