摘要:只要在調(diào)用異步函數(shù)時設(shè)置一個或多個回調(diào)函數(shù),函數(shù)就會在完成時自動調(diào)用回調(diào)函數(shù)。要解決的問題是,如何將回調(diào)方法的參數(shù)從回調(diào)方法中傳遞出來,讓它可以像同步函數(shù)的返回結(jié)果一樣,在回調(diào)函數(shù)以外的控制范圍內(nèi),可以傳遞和復(fù)用。
為什么需要異步方式摘要: 我們知道 JavaScript 自從有了 Generator 之后,就有了各種基于 Generator 封裝的協(xié)程。其中 hprose 中封裝的 Promise 和協(xié)程庫實現(xiàn)了跟 ES2016 的 async/await 一樣的功能,并且更加靈活。我們還知道 PHP 自從 5.5 之后,也引入了 Generator,同樣也有了各種基于它封裝的 PHP 協(xié)程庫,hprose 同樣也為 PHP 提供的跟 JavaScript 版本類似的 Promise 和協(xié)程庫。下面我們就來看一下它跟 swoole 結(jié)合的效果。
一個函數(shù)執(zhí)行之后,在它后面順序編寫的代碼中,如果能夠直接使用它的返回結(jié)果或者它修改之后的引用參數(shù),那么我們通常認為該函數(shù)是同步的。
而如果一個函數(shù)的執(zhí)行結(jié)果或者其修改的引用參數(shù),需要通過設(shè)置回調(diào)函數(shù)或者回調(diào)事件的方式來獲取,而在其后順序編寫的代碼中無法直接獲取的話,那么我們通常認為這樣的函數(shù)是異步的。
PHP 提供的大部分函數(shù)都是同步的。通常我們會有一個誤解,那就是容易把同步和阻塞當(dāng)成同一個概念,但實際上同步代碼不一定都是阻塞的,只是同步代碼對阻塞天然友好,當(dāng)同步代碼和阻塞結(jié)合時,代碼通常是簡單易懂的。
阻塞帶來的問題是當(dāng)前線程(或進程)會陷入等待,一直等到阻塞結(jié)束,這樣就會造成線程(或進程)資源的浪費。所以,通常認為阻塞是不夠高效的。
但是如果要編寫非阻塞代碼,使用同步方式會變得有些復(fù)雜,且不夠靈活。同步方式的非阻塞代碼通常會使用 select 模式,例如 curl_multi_select, stream_select, socket_select 等就是 PHP 中提供的一些典型的 select 模式的函數(shù)。
我們說它復(fù)雜且不夠靈活是有理由的,例如使用上面的 select 模式編寫同步的非阻塞代碼時,我們需要先構(gòu)造一個并發(fā)任務(wù)的列表,之后手動構(gòu)造循環(huán)來執(zhí)行這些并發(fā)的任務(wù),在循環(huán)開始之后,雖然這幾個任務(wù)可以并發(fā),但是這個循環(huán)相對于其后的代碼總體上仍然是阻塞的,我們要想拿到這些并發(fā)任務(wù)的結(jié)果時,仍然需要等待。select 雖然可以同時等待多個任務(wù)中某一個或幾個就位后,再執(zhí)行后續(xù)操作,但仍然有一部分時間是被等待消耗掉的。而且如果是純同步非阻塞的情況下,我們也很難在循環(huán)開始后,動態(tài)添加更多的任務(wù)到這個循環(huán)中去。
所以,如果我們希望程序能夠更加高效,更加靈活,就需要引入異步方式。
傳統(tǒng)的異步方式有什么問題一提到異步模式,大家腦子中的第一印象可能就是回調(diào)、回調(diào)、回調(diào)。是的,這是最簡單最直接也是之前最常見的異步模式。只要在調(diào)用異步函數(shù)時設(shè)置一個或多個回調(diào)函數(shù),函數(shù)就會在完成時自動調(diào)用回調(diào)函數(shù)。或者為一個對象設(shè)置一堆事件,之后調(diào)用該對象上的某個異步方法,雖然這個異步方法本身可能不再需要設(shè)置回調(diào)函數(shù),但是設(shè)置的這堆事件實際上跟回調(diào)函數(shù)所起到的作用是一樣的。
如果你的程序邏輯夠簡單,簡單的一兩層回調(diào)也許并不會讓你覺得異步方式的編程有什么麻煩。但如果你的程序邏輯一旦有些復(fù)雜,你可能就會被層層回調(diào)搞得疲憊不堪了。當(dāng)然,實際上你的程序需要層層回調(diào)的原因,也許并不是你的程序邏輯真的復(fù)雜,而是你沒有辦法將回調(diào)函數(shù)中的參數(shù)結(jié)果傳出來,所以,你就不得不將另一個回調(diào)函數(shù)傳進去。
我們來舉一個簡單的例子,假設(shè)我們有 1 個同步函數(shù):
function sum($a, $b) { return $a + $b; }
然后我們按照下面的方式去調(diào)用它:
$a = sum(1, 2); $b = sum($a, 3); $c = sum($b, 4); var_dump(array($a, $b, $c));
雖然上面的代碼很不精簡,但我們要表達的意圖很明確,而且代碼看起來很清楚。
那接下來我們把這個函數(shù)換成一個形式上的異步函數(shù),例如:
function async_sum($a, $b, $callback) { $callback($a + $b); }
當(dāng)然,它的執(zhí)行并不是異步的,這里我們先不關(guān)心它的實現(xiàn)是不是真異步的。
現(xiàn)在如果要做上面同樣的操作,代碼就要這樣寫了:
async_sum(1, 2, function($a) { async_sum($a, 3, function($b) use ($a) { async_sum($b, 4, function($c) use ($a, $b) { var_dump(array($a, $b, $c)); }); }); });
代碼的執(zhí)行結(jié)果是一樣的。但異步的代碼看起來顯然更難讀一些,雖然這已經(jīng)是很簡單的例子了。
好了,看到這里,有些讀者可能會覺的我上面的這個例子很糟糕。因為明明有同步的函數(shù)可以使用,并且代碼清晰可讀,為啥非要寫個形似異步的函數(shù),把本來同步可以做的很好的事情用異步方式復(fù)雜化呢?而且那個異步調(diào)用的方式,最后不還是想要實現(xiàn)同步化的結(jié)果嗎?
如果你這么想的話,一點都沒錯。但我們這里想要解決的問題是,如果我們拿到的只有一個異步函數(shù),這個函數(shù)沒有同步實現(xiàn),我們也不知道這個異步函數(shù)的內(nèi)部定義是怎樣的,我們也沒辦法將這個異步函數(shù)改為同步函數(shù)實現(xiàn)。那我們有沒有辦法將上面的程序改的更可讀一些呢?
當(dāng)然是可以的,所以,現(xiàn)在 Promise 要登場了。
為什么要引入 Promise通常我們對 Promise 的一個誤解就是,它要解決的是層層回調(diào)的問題,比如上面的問題看上去就是一個典型的層層回調(diào)的問題。
然而實際上,Promise 要解決的并不是回調(diào)不回調(diào)的問題,如果你使用過 Promise 的話,你會發(fā)現(xiàn)使用 Promise 你仍然少不了要使用回調(diào)。Promise 要解決的問題是,如何將回調(diào)方法的參數(shù)從回調(diào)方法中傳遞出來,讓它可以像同步函數(shù)的返回結(jié)果一樣,在回調(diào)函數(shù)以外的控制范圍內(nèi),可以傳遞和復(fù)用。
下面這幾篇文章可能會對大家理解 Promise 有所幫助:
深入理解 Promise 五部曲:1. 異步問題
深入理解 Promise 五部曲:2. 控制權(quán)轉(zhuǎn)換問題
深入理解 Promise 五部曲:3. 可靠性問題
深入理解 Promise 五部曲:4. 擴展問題
深入理解 Promise 五部曲:5. LEGO
我覺得這幾篇文章講的比較透徹,所以我就不重復(fù)文章中的內(nèi)容了。
下面我們來看上面的例子用 Promise 如何解。
我們現(xiàn)在用最簡單粗暴的方式來引入 Hprose 的庫,直接復(fù)制源碼而不是使用 composer。然后我們在代碼中直接使用:
這種方式來引入 Hprose 的 Promise 庫,當(dāng)然你也可以寫成:
Future 庫跟 Promise 庫基本上是一樣的,你可以認為 Future 是 Promise 的具體實現(xiàn),Promise 只是 Future 實現(xiàn)的一個包裝。這個區(qū)別你可以從源碼中直接看出來,這里就不多做解釋了。
接下來,我們要把前面的 async_sum 函數(shù) Promise 化,Hprose 提供了這樣一個函數(shù):Promisepromisify(或者 Futurepromisify),它的作用就是將一個使用回調(diào)方式的異步函數(shù)變成一個返回 Promise 對象的異步函數(shù)。這樣說,也許有些不好理解,下面直接上代碼:
then(function($a) use ($sum) { return $sum($a, 3); }); $c = $b->then(function($b) use ($sum) { return $sum($b, 4); }); Promiseall(array($a, $b, $c))->then(function($result) { var_dump($result); });好了,看到這里,如果你對 Promise 的理解還不夠深入的話,你的第一反應(yīng)可能是:這不是把程序變得更復(fù)雜了嗎?原來的程序是 3 個回調(diào),現(xiàn)在仍然是 3 個回調(diào),還多了包裝,都玩出花來了,有意思嗎?
確實,從上面的代碼來看,代碼并沒有被簡化,但是你會發(fā)現(xiàn),現(xiàn)在回調(diào)函數(shù)中的參數(shù)已經(jīng)通過 Promise 返回值的方式傳遞出來了,而且可以在原本的回調(diào)函數(shù)控制范圍以外被傳遞和復(fù)用了。
但是你可能會說然并卵,程序不是仍然很復(fù)雜嗎?那我們就來進一步簡化一下:
現(xiàn)在,代碼中再也看不到回調(diào)了。因為我們把函數(shù)包裝成了可以接收 Promise 變量的函數(shù)。當(dāng)然,其實現(xiàn)細節(jié)略微有些復(fù)雜,如果你感興趣,可以去看一下源碼,這里就不做源碼剖析了。如果感興趣的讀者多得話,以后有時間再寫源碼剖析。
當(dāng)然,如果你只是想把異步調(diào)用同步化,除了 Promisewrap 外,你還可以通過 co/yield 協(xié)程來實現(xiàn)。
Hprose 中的 co/yield 協(xié)程還是上面的例子,如果你使用的是 PHP 5.5 或者更高版本,那么你可以這樣來寫代碼了。
這代碼比使用 Promisewrap 的又要簡單了。這里,代碼中的變量 $a, $b, $c 不再是 Promise 變量,而是實實在在的整數(shù)變量。也就是說,yield 把一個 Promise 變量變成了一個普通變量。
現(xiàn)在 Promiseco 中的代碼已經(jīng)被實實在在的同步化了。
現(xiàn)在你可能有新的疑問了,異步不是為了高效嗎?現(xiàn)在把原本的異步代碼同步化了,那還會高效嗎?
當(dāng)然,對這個例子上來說,效率肯定是沒有提高,反而是嚴重降低的。甚至在這個例子中,最原始的那個形似異步的實現(xiàn)也不比同步實現(xiàn)更高效。因為在這個例子中,并沒有涉及到并發(fā)和 IO 阻塞的情況。
下面我們就放到真實場景下來看看 Promise 和 co/yield 協(xié)程是怎么用的。
在 swoole 下使用 Promise 和 co/yield 協(xié)程我們知道在 PHP 中,如果要讓程序延時可以使用 sleep 函數(shù)(或者 usleep, time_nanosleep 函數(shù))來讓程序阻塞一會兒,但是這個阻塞會讓整個進程都阻塞,所以在阻塞期間,什么都不能干。
下面我們來看看使用 swoole_timer_after 實現(xiàn)的延時執(zhí)行:
該程序執(zhí)行結(jié)果如下:
string(24) "wait 1s, now is 13:48:25" string(24) "wait 2s, now is 13:48:26" string(24) "wait 1s, now is 13:48:26" string(24) "wait 1s, now is 13:48:27" string(24) "wait 2s, now is 13:48:28" string(24) "wait 1s, now is 13:48:28" string(24) "wait 1s, now is 13:48:29" string(24) "wait 2s, now is 13:48:30" string(24) "wait 2s, now is 13:48:32" string(24) "wait 2s, now is 13:48:34"從結(jié)果中我們可以看出,wait(2000) 和 wait(1000) 各自都是順序阻塞執(zhí)行的,但是它們之間卻是并發(fā)執(zhí)行的。
也就是說,協(xié)程之間并不會相互阻塞,雖然這幾個并發(fā)的協(xié)程是在同一個進程內(nèi)跑的。
最后我們再來看一個用 co/yield 協(xié)程實現(xiàn)的并發(fā)抓圖程序:
setHeaders([ "Host" => $host, "User-Agent" => "Chrome/49.0.2587.3", ]); $get = Promisepromisify([$cli, "get"]); yield $get($url["path"]); list($filename) = (yield $writefile(basename($url["path"]), $cli->body)); echo "write $filename ok. "; $cli->close(); } $urls = array( "http://b.hiphotos.baidu.com/baike/c0%3Dbaike116%2C5%2C5%2C116%2C38/sign=5f4519ba037b020818c437b303b099b6/472309f790529822434d08dcdeca7bcb0a46d4b6.jpg", "http://f.hiphotos.baidu.com/baike/c0%3Dbaike116%2C5%2C5%2C116%2C38/sign=1c37718b3cc79f3d9becec62dbc8a674/38dbb6fd5266d016dc2eaa5c902bd40735fa358a.jpg", "http://h.hiphotos.baidu.com/baike/c0%3Dbaike116%2C5%2C5%2C116%2C38/sign=edd05c9c502c11dfcadcb771024e09b5/d6ca7bcb0a46f21f3100c52cf1246b600c33ae9d.jpg", "http://a.hiphotos.baidu.com/baike/c0%3Dbaike92%2C5%2C5%2C92%2C30/sign=4693756e8094a4c21e2eef796f9d70b0/54fbb2fb43166d22df5181f5412309f79052d2a9.jpg", "http://a.hiphotos.baidu.com/baike/c0%3Dbaike92%2C5%2C5%2C92%2C30/sign=9388507144a98226accc2375ebebd264/faf2b2119313b07eb2cc820c0bd7912397dd8c45.jpg", ); foreach ($urls as $url) { Promiseco(fetch($url)); }在這個程序中,fetch 函數(shù)內(nèi)的代碼是同步執(zhí)行的,但是多個 fetch 之間卻是并發(fā)執(zhí)行的,從結(jié)果輸出就可以看出來,輸出順序是不一定的。但最后,你總能得到所有的美圖。
總結(jié):通過 swoole 跟 hprose 中的 Promise 和 co/yield 協(xié)程相結(jié)合,你可以方便的使用同步的方式來調(diào)用 swoole 中的異步函數(shù)和方法,并可以實現(xiàn)協(xié)程間的并發(fā)。
因為篇幅所限,這里無法把 hprose 中 Promise 和 co/yield 協(xié)程的全部內(nèi)容都介紹完,如果你想了解更多,可以參考下面兩篇內(nèi)容:
Promise 異步編程
co/yield 協(xié)程
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/22138.html
摘要:前言入門阮一峰另類的實現(xiàn)同級別的另外一個函數(shù)。該事件系統(tǒng)允許代碼定義應(yīng)用程序的特定事件,該事件可以傳遞自定義參數(shù),自定義參數(shù)包含訂閱者所需要的值。其目的是避免訂閱者和發(fā)布者產(chǎn)生依賴關(guān)系。狀態(tài)轉(zhuǎn)變不可逆。方法必須返回一個。 callback 前言 ECMAScript 6入門(阮一峰) showImg(https://segmentfault.com/img/remote/1460000...
摘要:課前學(xué)習(xí)對象阮一峰廖雪峰從當(dāng)年的回調(diào)地獄到語法糖再到,通過不斷的進化來更好的從代碼層面同步方式寫異步操作下面以語法糖為例介紹如何實現(xiàn)首先寫個基于 Promise 課前學(xué)習(xí)Promise 對象 阮一峰http://javascript.ruanyifeng....Promise 廖雪峰https://www.liaoxuefeng.com/w... js從當(dāng)年的回調(diào)地獄、到co語法糖再...
摘要:本文先回顧生成器,然后過渡到協(xié)程編程。其作用主要體現(xiàn)在三個方面數(shù)據(jù)生成生產(chǎn)者,通過返回數(shù)據(jù)數(shù)據(jù)消費消費者,消費傳來的數(shù)據(jù)實現(xiàn)協(xié)程。解決回調(diào)地獄的方式主要有兩種和協(xié)程。重點應(yīng)當(dāng)關(guān)注控制權(quán)轉(zhuǎn)讓的時機,以及協(xié)程的運作方式。 轉(zhuǎn)載請注明文章出處: https://tlanyan.me/php-review... PHP回顧系列目錄 PHP基礎(chǔ) web請求 cookie web響應(yīng) sess...
摘要:函數(shù)并不是生成器協(xié)程函數(shù)自動執(zhí)行的唯一方案。因為自動執(zhí)行的關(guān)鍵是,必須有一種機制,自動控制生成器協(xié)程函數(shù)的流程,接收和交還程序的執(zhí)行權(quán)。回調(diào)函數(shù)可以做到這一點,對象也可以做到這一點。本系列的下一篇,將介紹基于的實現(xiàn)的自動執(zhí)行器。 PHP下的異步嘗試系列 如果你還不太了解PHP下的生成器和協(xié)程,你可以根據(jù)下面目錄翻閱 PHP下的異步嘗試一:初識生成器 PHP下的異步嘗試二:初識協(xié)程 P...
摘要:結(jié)果打印我結(jié)論或問題這里我們基礎(chǔ)實現(xiàn)了一個可以用于生產(chǎn)環(huán)境的后續(xù)我們會接續(xù)完善這個的特有方法,比如等后續(xù)再介紹用實現(xiàn)的自動執(zhí)行器等附錄參考中文對象入門阮一峰 PHP下的異步嘗試系列 如果你還不太了解PHP下的生成器和協(xié)程,你可以根據(jù)下面目錄翻閱 PHP下的異步嘗試一:初識生成器 PHP下的異步嘗試二:初識協(xié)程 PHP下的異步嘗試三:協(xié)程的PHP版thunkify自動執(zhí)行器 PHP下的...
閱讀 2943·2023-04-26 01:49
閱讀 2074·2021-10-13 09:39
閱讀 2287·2021-10-11 11:09
閱讀 928·2019-08-30 15:53
閱讀 2821·2019-08-30 15:44
閱讀 927·2019-08-30 11:12
閱讀 2980·2019-08-29 17:17
閱讀 2378·2019-08-29 16:57