PHP多進程
1.多開幾個進程,這種方式簡單實用垦细,推薦,比如說使用shell腳本:
#!/bin/bash
for((i=1;i<=8;i++))
do
/usr/bin/php multiprocessTest.php &
done
wait
2.pcntl擴展
php多進程需要pcntl挡逼,posix擴展支持括改,可以通過 php -m 查看,而且多進程實現(xiàn)只能在cli模式下家坎,雖然是個殘廢嘱能,不妨也了解一下, 實際上這些都是調(diào)用了Linux的系統(tǒng)API
舉個例子:
<?php
foreach (range(1, 5) as $index) {
$pid = pcntl_fork();
if ($pid === -1) {
echo "failed to fork!\n";
exit;
} elseif ($pid) {
pcntl_wait($status); //父進程必須等待一個子進程退出后,再創(chuàng)建下一個子進程虱疏。
echo "I am the parent, pid: $pid\n";
} else {
$cid = posix_getpid();
echo "fork the {$index}th child, pid: $cid\n";
exit; //必須
}
}
這個例子非常簡單惹骂,循環(huán)創(chuàng)建5個進程,在各個進程里面打印一句話做瞪,主要使用的方法就是函數(shù) pcntl_fork对粪,一次調(diào)用兩次返回,在父進程中返回子進程pid装蓬,在子進程中返回0著拭,出錯返回-1。
執(zhí)行結(jié)果如下:
fork the 1th child, pid: 7326
I am the parent, pid: 7326
fork the 2th child, pid: 7327
I am the parent, pid: 7327
fork the 3th child, pid: 7328
I am the parent, pid: 7328
fork the 4th child, pid: 7329
I am the parent, pid: 7329
fork the 5th child, pid: 7330
I am the parent, pid: 7330
先解釋一下為什么會產(chǎn)生10條打印結(jié)果牍帚,第一條結(jié)果是子進程打印的儡遮,第二條是在父進程打印的!
第一個坑:
如果是在循環(huán)中創(chuàng)建子進程,那么子進程中最后要exit,防止子進程進入循環(huán)!
第二個坑:
必須等待子進程執(zhí)行完任務(wù), 有一個簡單方法是使用 pcntl_wait暗赶,如果不加這個你會發(fā)現(xiàn)一個是執(zhí)行的順序不固定旨涝,第二個就是創(chuàng)建的進程會少于5個,但是加了你會發(fā)現(xiàn)這個完全變成并行了...上面的結(jié)果就是
然后找了找踏堡,發(fā)現(xiàn)下面這種寫法:
<?php
$ids = [];
foreach (range(1, 5) as $index) {
$ids[] = $pid = pcntl_fork();
if ($pid === -1) {
echo "failed to fork!\n";
exit;
} elseif ($pid) {
echo "I am the parent, pid: $pid\n";
} else {
$cid = posix_getpid();
echo "fork the {$index}th child, pid: $cid\n";
exit;
}
}
foreach ($ids as $i => $pid) {
if ($pid) {
pcntl_waitpid($pid, $status);
}
}
結(jié)果如下:
fork the 1th child, pid: 8392
I am the parent, pid: 8392
I am the parent, pid: 8393
fork the 2th child, pid: 8393
I am the parent, pid: 8394
I am the parent, pid: 8395
I am the parent, pid: 8396
fork the 3th child, pid: 8394
fork the 4th child, pid: 8395
fork the 5th child, pid: 8396
找了一張圖,大體解釋了總體流程:
說簡單其實也挺簡單,幾行代碼就可以寫出一個多進程程序,實現(xiàn)并行編程蓝翰,但是這里其實還有不少坑,比如僵尸進程模孩,孤兒進程, 守護進程厌小,具體的我也不太熟悉不多講,再看一個關(guān)于進程信號的東西浸锨,有些項目里面有時候會用到一些腳本唇聘,比如處理redis隊列的腳本,通常的做法是寫一個while死循環(huán)一直從redis里面取數(shù)據(jù)處理柱搜,為了防止內(nèi)存泄露或者假死迟郎,一般都會定時的殺掉腳本重啟腳本,但是殺的不好可能會導(dǎo)致數(shù)據(jù)丟失聪蘸,舉個例子宪肖,假如你這個腳本剛好從redis取了一條數(shù)據(jù)正在處理中表制,操作還未完成,你突然終止進程控乾,那這個數(shù)據(jù)就丟失了么介。至于說服務(wù)器掛掉這種情況畢竟不多見,真要解決這種問題還得從隊列上入手蜕衡。
<?php
//ctrl+c
pcntl_signal(SIGINT, function () {
fwrite(STDOUT, "receive signal: " . SIGINT . " do nothing ...\n");
});
//kill
pcntl_signal(SIGTERM, function () {
fwrite(STDOUT, "receive signal: " . SIGTERM . " I will exit!\n");
exit;
});
while (true) {
pcntl_signal_dispatch();
echo "do something壤短。。慨仿。\n";
sleep(5);
}
Linux進程信號分為很多種久脯,kill -l 可以查看,PHP里面定義了43種镰吆,咱就說說常用的幾種:
SIGINT 2 這個其實相對于 ctrl+c
SIGTERM 15 就是 kill 默認的參數(shù)帘撰,表示終止信號,但是你發(fā)了信號程序不一定響應(yīng)
SIGKILL 9 就是 kill -9, 表示立馬終止万皿,這個信號在PHP里面是無法注冊的骡和,所以一定能成功
看明白了這個就可以讀懂上面的例子了,其中 pcntl_signal 是注冊信號處理handler相寇,第一個參數(shù)是你需要注冊的信號慰于,第二個是處理操作,可以是匿名函數(shù)或者一個函數(shù)名唤衫,可以注冊多個信號婆赠。pcntl_signal_dispatch 調(diào)用每個等待信號通過pcntl_signal() 安裝的處理器。早期PHP還有一種寫法是使用 ticks佳励,性能非常差休里,php5.3之后建議都使用 pcntl_signal_dispatch。
說明一下:pcntl_signal()函數(shù)僅僅是注冊信號和它的處理方法赃承,真正接收到信號并調(diào)用其處理方法的是pcntl_signal_dispatch()函數(shù)必須在循環(huán)里調(diào)用妙黍,為了檢測是否有新的信號等待dispatching。
上面的例子執(zhí)行結(jié)果就是當你使用 ctrl+c 的話是無法終止程序的瞧剖,只有使用 kill pid 這種形式才可以拭嫁,但是并不是立馬就退出,它是代碼執(zhí)行到循環(huán)頂部 pcntl_signal_dispatch 地方的時候才會退出抓于,這就保證了你使用kill殺掉進程的時候并不會丟失數(shù)據(jù)做粤,說好聽點這也算是平滑重啟吧!
由于進程的系統(tǒng)開銷比較大捉撮,一般不太適合拿來做大規(guī)模并發(fā)程序怕品,拿來寫個3-5個進程的后臺腳本倒是有點用,下面就是我寫的一個用來爬取xhprof的數(shù)據(jù)的腳本巾遭,使用了3個進程同時爬取實戰(zhàn)肉康,路徑闯估,免費課的日志然后做統(tǒng)計根據(jù)出現(xiàn)次數(shù)排序!
<?php
define("TOTAL_PAGE", 100); //總共多少頁
define("MS", 2000); //毫秒
define("DAY", 3); //幾天內(nèi)
define("SAVE_DIR", "/home/jwang"); //保存目錄
$servers = [
'mkw' => '10.100.133.99',
'sz' => '10.100.135.23',
'lj' => '10.100.17.13',
];
$ids = [];
foreach ($servers as $key => $server) {
$ids[] = $pid = pcntl_fork();
if ($pid === -1) {
echo "failed to fork!\n";
exit;
} elseif ($pid) {
} else {
download($server, $key);
}
}
foreach ($ids as $i => $pid) {
if ($pid) {
pcntl_waitpid($pid, $status);
}
}
function download($server, $fileName)
{
$saveDir = SAVE_DIR;
if (!is_dir(SAVE_DIR)) {
$saveDir = __DIR__;
}
$file = $saveDir . "/xhprof_{$fileName}_tmp.txt";
$fp = fopen($file, 'w+');
foreach (range(1, TOTAL_PAGE) as $page) {
print_r("### " . date('Y-m-d H:i:s') . ": 正在爬取 $server -> $fileName 第 $page 頁...\n");
try {
$html = file_get_contents("http://{$server}/xhprof/index.php?page={$page}&ms=" . MS . "&day=" . DAY);
} catch (Exception $exception) {
var_dump("網(wǎng)絡(luò)請求失敗!\n");
exit;
}
if (!$html) {
var_dump("網(wǎng)絡(luò)請求失敗!\n");
exit;
}
preg_match_all("/<a .*>(.*)<\\/a>/", $html, $matches);
if (isset($matches[1])) {
if (count($matches[1]) <= 3) {
break;
}
foreach ($matches[1] as $match) {
fwrite($fp, $match . "\n");
}
}
}
fclose($fp);
print_r("### " . date('Y-m-d H:i:s') . ": 爬取完成吼和,開始處理數(shù)據(jù)...\n");
print_r("---------------------------------------------------------- \n");
$fp = file($file);
if (!$fp) {
var_dump("文件讀取失敗!\n");
}
foreach ($fp as $key => $item) {
$item = rtrim(parse_url(trim($item))['path'], "/");
if (substr($item, 0, 1) != '/') {
unset($fp[$key]);
continue;
}
$fp[$key] = preg_replace("/\/\d+/", "/*", $item);
}
$res = array_count_values($fp);
uasort($res, function ($a, $b) {
return $a < $b;
});
$saveFile = fopen($saveDir . "/xhprof_{$fileName}.txt", 'w+');
foreach ($res as $key => $value) {
$key = trim($key);
$str = sprintf("%-50s ===============> %s 次\n", $key, $value);
fwrite($saveFile, $str);
}
fclose($saveFile);
unlink($file);
exit;
}
最后還忘記說了一個坑涨薪,在子進程里面使用mysql 或者 redis 這類程序有個bug,假如你使用的是單例模式的話纹安,這個連接被多個子進程使用就會出問題,所以如果要使用砂豌,必須在各個子進程內(nèi)部新建一個連接厢岂!