PHP是世界上最好的語言,但是總被“同行們”吐槽不支持異步。其實(shí)我們要實(shí)現(xiàn)異步也非常簡單,之前看到鳥哥的一篇寫PHP異步執(zhí)行的博文 PHP實(shí)現(xiàn)異步調(diào)用方法研究,這篇文章還是08年的,到今天PHP發(fā)展快10年了,對于異步調(diào)用也有了更多新的玩法。
一. 先說說鳥哥文章中的幾種玩法:
一是通過渲染前端頁面,使用js執(zhí)行Ajax,這種方式現(xiàn)在還適用。只是受限于業(yè)務(wù)場景,因?yàn)橹荒茉跒g覽器中調(diào)用,遇到接口請求就不行了。
二是通過popen()方法打開一個(gè)指向進(jìn)程的管道,每個(gè)請求會多起一個(gè)進(jìn)程。忽略進(jìn)程來看最主要的原因是數(shù)據(jù)的傳輸特別不方便,使用場景有限。
三是使用CURL擴(kuò)展,通過設(shè)置timeout超時(shí)參數(shù),能實(shí)現(xiàn)離弦之箭的效果。不過這種方法會主動斷開連接。被調(diào)用的服務(wù)如果有做連接檢測,也會中斷服務(wù)端腳本的執(zhí)行。比如我們請求 微信的某個(gè)費(fèi)時(shí)接口(20s),我們調(diào)用1s就斷開連接,微信端是否會維持請求執(zhí)行20S是不可控的。所以這種方法不推薦大家使用。
四方法與CURL類似,通過fsockopen創(chuàng)建socket連接訪問遠(yuǎn)程服務(wù),不循環(huán)獲取請求結(jié)果。一樣會有三中連接被斷開的問題。
二. PHP發(fā)展了這么多年對異步支持方面都有哪些改進(jìn)?
CURL擴(kuò)展已支持毫秒配置,將 CURLOPT_TIMEOUT 改為 CURLOPT_TIMEOUT_MS 即可生效(cURL 版本 >= libcurl/7.21.0,老服務(wù)器要檢查版本),但還是我前面說的需要服務(wù)端配合,不然接口的調(diào)用成功失敗不可控。
CURL擴(kuò)展已支持并發(fā),我們能一次訪問N個(gè)接口,執(zhí)行時(shí)間取最長接口的時(shí)間。比如我們能一次訪問 京東支付(1s),微信支付(1.2s),支付寶(0.8s)不同服務(wù)的三個(gè)接口,總耗時(shí)才1.2s。詳細(xì)用法 curl_multi_init
類似Node.js的異步IO框架Swoole,能很好的實(shí)現(xiàn)異步調(diào)用;不過Swoole理論上不能算PHP框架,他算是PHP功能的擴(kuò)展。所以除非項(xiàng)目都用Swoole寫,不然也是享受不到異步IO的福利。
對yield的支持,能實(shí)現(xiàn)調(diào)度器的功能,寫單進(jìn)程的服務(wù)時(shí)能大展拳腳,特別是實(shí)現(xiàn)協(xié)程,異步更不在話下。不過在多進(jìn)程的web服務(wù)上沒有太大的使用場景,看未來會不會有新的玩法吧。
當(dāng)然還有很多新的特性,這里不再細(xì)說,總之PHP越是被黑越是能快速發(fā)展。
三. 最好的異步實(shí)現(xiàn)方法
我們都知道PHP是支持多進(jìn)程編程的,那完全可以新建一個(gè)進(jìn)程去實(shí)現(xiàn)異步的調(diào)用。比如調(diào)用popen()方法,但是管道的方式傳參異常麻煩,不過多進(jìn)程這個(gè)方法是絕對可行的。如果要實(shí)現(xiàn)多進(jìn)程的功能,毫無疑問我們會選擇PHP官方提供的 pcntl 擴(kuò)展,PHP默認(rèn)會安裝pcntl擴(kuò)展,如果代碼運(yùn)行提示找不到pcntl擴(kuò)展,可自行到php-src下載,選擇好版本通過phpize安裝即可。代碼如下
<?php
/**
* User: layne.xfl
* Date: 2017/5/12
* Time: 下午01:24
*/
class Arrow{
static $instance;
/**
* @return static
*/
public static function getInstance(){
if (null == Arrow::$instance)
Arrow::$instance = new Arrow();
return Arrow::$instance;
}
public function run($rb){
$pid = pcntl_fork();
if($pid > 0){
pcntl_wait($status);
}elseif($pid == 0){
$cid = pcntl_fork();
if($cid > 0){
exit();
}elseif($cid == 0){
$rb();
}else{
exit();
}
}else
{
exit();
}
}
}
//離弦之箭---調(diào)用方法
$time_out = 30;
Arrow::getInstance()->run(function() use ($time_out){
//這里寫我們要執(zhí)行的代碼
sleep($time_out);
});
我給這個(gè)功能取了一個(gè)很生動的名字--離弦之箭。代表異步調(diào)用,我們的弓箭射出去后并不關(guān)心它的結(jié)果因?yàn)榘l(fā)送這個(gè)動作做了就行。比如打個(gè)10M的log,通知10個(gè)人(發(fā)10條短信)。
代碼說明:首先Arrow類是個(gè)單例類,減少多次調(diào)用的開銷。run()方法傳遞的是一個(gè)匿名函數(shù),這樣我們能非常方便的傳遞參數(shù),并且保留上下文。
這個(gè)類最難的地方在于多進(jìn)程的處理。因?yàn)槲覀円M可能快的將數(shù)據(jù)返回給用戶,所以主進(jìn)程越快結(jié)束越好。但是我們又需要子進(jìn)程來執(zhí)行我們耗時(shí)的操作,執(zhí)行完退出才行。如果不等子進(jìn)程執(zhí)行完就將父進(jìn)程退出會出現(xiàn)什么結(jié)果呢?結(jié)果就是子進(jìn)程會常駐內(nèi)存變成僵死進(jìn)程。那我們有什么辦法讓子進(jìn)程執(zhí)行完之后就自動結(jié)束呢?答案是很難……那么兒子進(jìn)程這么不聽話,孫子進(jìn)程會不會聽話一點(diǎn)呢??答案是孫子進(jìn)程執(zhí)行結(jié)束后會被系統(tǒng)進(jìn)程回收并銷毀(還是孫子聽話)。所以我在代碼中使用了如下方法:當(dāng)前請求進(jìn)程fork出子進(jìn)程,子進(jìn)程fork出孫子進(jìn)程,主進(jìn)程和子進(jìn)程都先行退出,最后由孫子進(jìn)程來執(zhí)行耗時(shí)操作,最后完美的解決了僵死進(jìn)程問題。
當(dāng)然這個(gè)方法的缺點(diǎn)就是調(diào)用的時(shí)候會多產(chǎn)生一個(gè)php-fpm的進(jìn)程。關(guān)于php-fpm進(jìn)程的管理和規(guī)劃又是另一個(gè)話題了。擴(kuò)展閱讀 PHP進(jìn)程間通信
更多相關(guān)文章請移步我的博客-原文鏈接:PHP異步的的玩法
喜歡編程的朋友可以關(guān)注我的個(gè)人公眾號,保證每周三篇原創(chuàng)。