異步函數(shù)的兩個(gè)視角

我們來(lái)一起看一下兩個(gè)程序員之間的故事。

以下示例代碼是用Scala寫(xiě)的教藻,不過(guò)本文所講的話題并不僅限于Scala,任何有Future/Promise支持的語(yǔ)言都是適用的告组。

下面這個(gè)wiki頁(yè)面羅列了各個(gè)有Future/Promise支持的語(yǔ)言,已經(jīng)涵蓋了大多數(shù)的常用語(yǔ)言籍滴。

Future與promise實(shí)現(xiàn)列表

我是異步函數(shù)的編寫(xiě)者

我寫(xiě)了兩個(gè)異步函數(shù),來(lái)提供給其他程序員同事使用待逞。

type CallBack = Try[String] => Unit

def pretendCallAPI(callBack: CallBack, okMsg: String, failedMsg: String) = {
  val task = new TimerTask {
    override def run() = {
      val percentage = Random.between(1, 100)

      if (percentage >= 50)
        callBack(Success(okMsg))
      else if (percentage <= 30)
        callBack(Failure(new Exception(failedMsg)))
      else
        callBack(Failure(new Exception("network problem")))
    }
  }

  new Timer().schedule(task, Random.between(200, 500))
}

val searchTB = pretendCallAPI(_, "product price found", "product not listed")
val buyFromTB = pretendCallAPI(_, "product bought", "can not buy, no money left")

這兩個(gè)異步函數(shù): searchTB用來(lái)從淘寶搜索物品,另一個(gè)buyFromTB用來(lái)購(gòu)買(mǎi)搜到的物品轩缤。

由于僅僅是為了演示而寫(xiě)的,他們兩個(gè)都是基于一個(gè)叫做pretendCallAPI的函數(shù)實(shí)現(xiàn)的转唉。

顧名思義,pretendCallAPI并不會(huì)真的去調(diào)用淘寶的API,而只是模擬API的行為皮钠。

這個(gè)pretendCallAPI函數(shù)有幾個(gè)行為特征:

  • 每次耗時(shí)200到500毫秒之間
  • 每次執(zhí)行有50%的幾率成功
  • 20%的幾率遇到網(wǎng)絡(luò)故障
  • 另外30%的幾率雖然網(wǎng)絡(luò)沒(méi)問(wèn)題但是服務(wù)器會(huì)給你一個(gè)非正常的結(jié)果

當(dāng)然,由于我寫(xiě)的是異步算法,需要避免block caller thread。

所以當(dāng)你調(diào)用pretendCallAPI的時(shí)候,這個(gè)函數(shù)是瞬間立即返回的赠法。

那么當(dāng)然我就無(wú)法在函數(shù)返回的時(shí)候return什么有用的東西給你了鳞芙。

如果你想知道執(zhí)行的結(jié)果到底是啥,你需要傳給我一個(gè)CallBack,在我執(zhí)行完后,通過(guò)CallBack來(lái)告知你執(zhí)行的結(jié)果。

這個(gè)CallBack的完整簽名表達(dá)式展開(kāi)是Try[String] => Unit

大家看searchTB和buyFromTB可能覺(jué)得他們長(zhǎng)的有點(diǎn)奇怪,這是Scala里柯里化的寫(xiě)法期虾。

也就是通過(guò)把pretendCallAPI包一層來(lái)構(gòu)造新的函數(shù),鎖死兩個(gè)參數(shù),剩下的一個(gè)參數(shù)(也就是CallBack)就變成了新構(gòu)造出來(lái)的函數(shù)的唯一參數(shù)了。

也就是說(shuō)searchTB和buyFromTB的簽名是(Try[String] => Unit) => Unit驯嘱。

關(guān)于柯里化這個(gè)語(yǔ)言特性的更多信息:

https://cuipengfei.me/blog/2013/12/25/desugar-scala-6/

好了,現(xiàn)在這兩個(gè)函數(shù)可以提供給大家使用了镶苞。

我是異步函數(shù)的調(diào)用者

聽(tīng)說(shuō)異步函數(shù)已經(jīng)寫(xiě)好了,我終于可以用他們來(lái)實(shí)現(xiàn)剁手業(yè)務(wù)了。

聽(tīng)函數(shù)作者講了一下,用起來(lái)應(yīng)該不會(huì)很難,那我來(lái)實(shí)現(xiàn)一下吧鞠评。

def searchPriceThenBuy() = {
  searchTB {
    case Success(searchMsg) =>
      println(searchMsg)
      buyFromTB {
        case Success(buyMsg) => println(buyMsg)
        case Failure(err) => println(err.getMessage)
      }
    case Failure(err) => println(err.getMessage)
  }
}

使用searchTB和buyFromTB并不難. 他們兩個(gè)都是接受CallBack作為參數(shù)的函數(shù)茂蚓。

CallBack本身是個(gè)函數(shù),它的簽名是Try[String] => Unit。

而Try有兩種形式,分別是Success和Failure剃幌。

所以在調(diào)用searchTB和buyFromTB的時(shí)候,必須把兩個(gè)分支都給到(以免pattern match不到)聋涨。

這樣在異步函數(shù)有結(jié)果的時(shí)候(無(wú)論成敗)才能call back過(guò)來(lái)到我的代碼,以便我能夠在合適的時(shí)機(jī)做后續(xù)的處理(無(wú)論是基于成功做后續(xù)業(yè)務(wù),還是做error handling)。

關(guān)于pattern match,可以參考這里:

https://cuipengfei.me/blog/2013/12/29/desugar-scala-8/

https://cuipengfei.me/blog/2015/06/16/visitor-pattern-pattern-match/

這段代碼跑一下的話,會(huì)有這么幾種結(jié)果:

  • 搜到了,也買(mǎi)到了
  • 搜到了,購(gòu)買(mǎi)時(shí)遇到了網(wǎng)絡(luò)故障
  • 搜到了,由于支付寶錢(qián)不夠而沒(méi)買(mǎi)到
  • 沒(méi)搜到,購(gòu)買(mǎi)行為未觸發(fā)
  • 搜索遇到網(wǎng)絡(luò)故障,購(gòu)買(mǎi)行為未觸發(fā)

一共就這么幾種可能,因?yàn)閜retendCallAPI是跑概率的,多跑幾次這些情況都能遇到负乡。

雖然實(shí)現(xiàn)出來(lái)不難,執(zhí)行結(jié)果也沒(méi)問(wèn)題,但是總有點(diǎn)隱憂牍白。

這里只有searchTB和buyFromTB兩個(gè)函數(shù),如果其他場(chǎng)景下我需要把更多的異步函數(shù)組合起來(lái)使用呢?豈不是要縮進(jìn)很多層?

當(dāng)然,縮進(jìn)只是個(gè)視覺(jué)審美問(wèn)題,是個(gè)表象,不是特別要緊.關(guān)鍵是我的業(yè)務(wù)邏輯很容易被這樣的代碼給割裂的雞零狗碎,那就不好了。

我要給上游編寫(xiě)異步函數(shù)的同事反饋一下抖棘,看是否有辦法解決這個(gè)問(wèn)題茂腥。

鏡頭切回到異步函數(shù)編寫(xiě)者

之前寫(xiě)的兩個(gè)函數(shù)反饋不太好,主要是因?yàn)橥聜冋J(rèn)為使用CallBack不是最優(yōu)的方式切省。

這個(gè)反饋確實(shí)很中肯最岗,如果只有一個(gè)異步函數(shù)單獨(dú)使用,用CallBack也沒(méi)什么太大的問(wèn)題朝捆,如果是很多個(gè)異步函數(shù)組合使用確實(shí)會(huì)形成多層嵌套的問(wèn)題般渡。

我作為上游程序員,確實(shí)需要更多地為下游調(diào)用者考慮。

既然如此驯用,那我改版一下脸秽,免除掉讓下游使用CallBack的必要性。

type CallBackBasedFunction = (CallBack) => Unit

def futurize(f: CallBackBasedFunction) = () => {
  val promise = Promise[String]()

  f {
    case Success(msg) => promise.success(msg)
    case Failure(err) => promise.failure(err)
  }

  promise.future
}

val searchTBFutureVersion = futurize(searchTB)
val buyFromTBFutureVersion = futurize(buyFromTB)

先定義一個(gè)CallBackBasedFunction晨汹,它代表一個(gè)接受CallBack為參數(shù)的函數(shù)的簽名豹储。

表達(dá)式展開(kāi)后就是: (Try[String] => Unit) => Unit

這就符合了searchTB和buyFromTB兩個(gè)函數(shù)的簽名。

futurize算是個(gè)higher order function,它接受一個(gè)CallBackBasedFunction作為參數(shù)淘这,返回一個(gè)() => Future[String]剥扣。

(Future是Scala標(biāo)準(zhǔn)庫(kù)的內(nèi)容,可以認(rèn)為和JS Promises/A+是類(lèi)似的概念)

也就是說(shuō)futurize可以把searchTB和buyFromTB改造成返回Future的函數(shù)铝穷。上面代碼最后兩行就是改造的結(jié)果钠怯。

這樣,原本接受CallBack做為參數(shù)且沒(méi)有返回值的函數(shù)曙聂,就變成了不接受參數(shù)且返回Future的函數(shù)晦炊。

再看futurize的具體實(shí)現(xiàn),它使用了Scala的Promise宁脊,讓返回的Future在原版函數(shù)成功時(shí)成功断国,在原版函數(shù)失敗時(shí)失敗。

這樣榆苞,我就得到了searchTBFutureVersion和buyFromTBFutureVersion這兩個(gè)仍然是立即瞬間返回稳衬,不會(huì)block caller thread的函數(shù)

關(guān)于Scala中Promise和Future的更多信息:

https://docs.scala-lang.org/overviews/core/futures.html

鏡頭再切到異步函數(shù)調(diào)用者

現(xiàn)在有了searchTBFutureVersion和buyFromTBFutureVersion坐漏,我來(lái)試著重新實(shí)現(xiàn)一次:

def searchPriceThenBuyFutureVersion() = {
  val eventualResult = for {
    searchResult <- searchTBFutureVersion().map(msg => println(msg))
    buyResult <- buyFromTBFutureVersion().map(msg => println(msg))
  } yield (searchResult, buyResult)

  eventualResult.onComplete {
    case Failure(err) => println(err.getMessage)
    case _ =>
  }
}

這里用到了Scala的for comprehension薄疚,編譯后會(huì)變成map,flatMap等等monadic operator赊琳。

而map,flatMap等操作符正是Scala中Future拿來(lái)做組合用的街夭。

這樣,用for把兩個(gè)返回Future的異步函數(shù)組織起來(lái)躏筏,形成一個(gè)新的Future板丽,然后在新的Future complete時(shí)統(tǒng)一處理異常。

關(guān)于for的更多信息:

https://cuipengfei.me/blog/2014/08/30/options-for/

這次實(shí)現(xiàn)的代碼與上次的行為是一致的,沒(méi)什么兩樣趁尼。

不過(guò)我的業(yè)務(wù)代碼從雞零狗碎變成了平鋪直敘平易近人檐什。

(這種效果在這里表現(xiàn)的并不是特別突出,不過(guò)很容易想象如果需要組合使用的異步函數(shù)更多一些的話弱卡,這種效果的好處就顯露出來(lái)了)

當(dāng)然了乃正,讓業(yè)務(wù)代碼易讀易懂主要還是要靠個(gè)人奮斗,而有了Promise和Future這種歷史進(jìn)程的推力婶博,則更有增益作用瓮具。

小結(jié)

最近在看Scala Reactive的一些內(nèi)容

想起了很久之前寫(xiě)過(guò)一篇叫做自己動(dòng)手實(shí)現(xiàn)Promises/A+規(guī)范的博客,用JS實(shí)現(xiàn)了一個(gè)簡(jiǎn)版的Promise:

https://cuipengfei.me/blog/2016/05/15/promise/

我在當(dāng)時(shí)的一段演示代碼里面寫(xiě)了兩句注釋?zhuān)?/p>

Promise的作用在于

  1. 給異步算法的編寫(xiě)者和使用者之間提供一種統(tǒng)一的交流手段
  2. 給異步算法的使用者提供一種組織代碼的手段,以便于將一層又一層嵌套的業(yè)務(wù)主流程變成一次一次的對(duì)then的調(diào)用

不過(guò)當(dāng)時(shí)的博客里只講了實(shí)現(xiàn)Promise規(guī)范的事情,并沒(méi)有詳細(xì)解釋過(guò)這兩句話。

既然又遇到了這個(gè)話題名党,于是寫(xiě)點(diǎn)Scala來(lái)把當(dāng)時(shí)沒(méi)展開(kāi)寫(xiě)到的內(nèi)容補(bǔ)充了一下叹阔。

上文的四個(gè)鏡頭展現(xiàn)了兩個(gè)角色的思考過(guò)程,通過(guò)這個(gè)過(guò)程其實(shí)也就解釋了上面兩句注釋的含義传睹。

1.給異步算法的編寫(xiě)者和使用者之間提供一種統(tǒng)一的交流手段

所謂統(tǒng)一的交流手段耳幢,其實(shí)就是異步函數(shù)的簽名問(wèn)題。

由于需要處理的業(yè)務(wù)五花八門(mén)欧啤,異步函數(shù)接受的參數(shù)列表沒(méi)法統(tǒng)一睛藻,但是返回值是可以統(tǒng)一的。

一個(gè)異步函數(shù)邢隧,接受了外界給的參數(shù)店印,立即瞬間返回一個(gè)Js的Promise或者Scala的Future(或者是任何語(yǔ)言中類(lèi)似概念的叫法)。

然后在異步任務(wù)執(zhí)行完的時(shí)候把Promise resolve/reject掉(讓Future success或者failure),借此來(lái)讓調(diào)用方的代碼知道該到了它跑后續(xù)處理的時(shí)候了倒慧。

這樣我們就獲得了一個(gè)sensible default按摘,無(wú)需在每次設(shè)計(jì)異步函數(shù)的時(shí)候都去商議該返回什么東西,該怎么獲得異步執(zhí)行的結(jié)果纫谅。

2.給異步算法的使用者提供一種組織代碼的手段,以便于將一層又一層嵌套的業(yè)務(wù)主流程變成一次一次的對(duì)then的調(diào)用
所謂組織代碼的手段炫贤,就是關(guān)于異步函數(shù)調(diào)用者的那兩個(gè)鏡頭的內(nèi)容了。

一開(kāi)始CallBack套著CallBack付秕,異步的味道很重兰珍,這體現(xiàn)出了代碼的組織方式在向代碼的技術(shù)實(shí)現(xiàn)低頭№锬粒或者說(shuō)是代碼的技術(shù)實(shí)現(xiàn)干擾了我行文的風(fēng)格

后來(lái)變成了看起來(lái)很像是消費(fèi)同步函數(shù)結(jié)果的寫(xiě)法励幼。從而讓我慣常的文風(fēng)得以保持汰寓。


文/ThoughtWorks 崔鵬飛 更多洞見(jiàn)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市苹粟,隨后出現(xiàn)的幾起案子有滑,更是在濱河造成了極大的恐慌,老刑警劉巖嵌削,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件毛好,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡苛秕,警方通過(guò)查閱死者的電腦和手機(jī)肌访,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)艇劫,“玉大人吼驶,你說(shuō)我怎么就攤上這事。” “怎么了蟹演?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵风钻,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我酒请,道長(zhǎng)骡技,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任羞反,我火速辦了婚禮布朦,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘苟弛。我一直安慰自己喝滞,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布膏秫。 她就那樣靜靜地躺著右遭,像睡著了一般。 火紅的嫁衣襯著肌膚如雪缤削。 梳的紋絲不亂的頭發(fā)上窘哈,一...
    開(kāi)封第一講書(shū)人閱讀 51,763評(píng)論 1 307
  • 那天,我揣著相機(jī)與錄音亭敢,去河邊找鬼滚婉。 笑死,一個(gè)胖子當(dāng)著我的面吹牛帅刀,可吹牛的內(nèi)容都是我干的让腹。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼扣溺,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼骇窍!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起锥余,我...
    開(kāi)封第一講書(shū)人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤腹纳,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后驱犹,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體嘲恍,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年雄驹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了佃牛。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡医舆,死狀恐怖吁脱,靈堂內(nèi)的尸體忽然破棺而出桑涎,到底是詐尸還是另有隱情,我是刑警寧澤兼贡,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布攻冷,位于F島的核電站,受9級(jí)特大地震影響遍希,放射性物質(zhì)發(fā)生泄漏等曼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一凿蒜、第九天 我趴在偏房一處隱蔽的房頂上張望禁谦。 院中可真熱鬧,春花似錦废封、人聲如沸州泊。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)遥皂。三九已至,卻和暖如春刽漂,著一層夾襖步出監(jiān)牢的瞬間演训,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工贝咙, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留样悟,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓庭猩,卻偏偏與公主長(zhǎng)得像窟她,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蔼水,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 一.非阻塞和異步 借用知乎用戶嚴(yán)肅的回答在此總結(jié)下震糖,同步和異步是針對(duì)消息通信機(jī)制,同步代表一個(gè)client發(fā)出一個(gè)...
    Daniel_adu閱讀 1,827評(píng)論 0 8
  • 在現(xiàn)代軟件開(kāi)發(fā)中徙缴,系統(tǒng)功能越來(lái)越復(fù)雜试伙,管理復(fù)雜度的方法就是分而治之嘁信,系統(tǒng)的很多功能可能會(huì)被切分為小的服務(wù)于样,對(duì)外提供...
    天堂鳥(niǎo)6閱讀 7,156評(píng)論 0 23
  • title標(biāo)題: A Web Crawler With asyncio Coroutinesauthor作者: A...
    彰樂(lè)樂(lè)樂(lè)樂(lè)閱讀 2,062評(píng)論 0 8
  • 1 什么是異步編程 通過(guò)學(xué)習(xí)相關(guān)概念,我們逐步解釋異步編程是什么潘靖。 1.1 阻塞 程序未得到所需計(jì)算資源時(shí)被掛起的...
    hugoren閱讀 2,658評(píng)論 2 10
  • 我是黑夜里大雨紛飛的人啊 1 “又到一年六月穿剖,有人笑有人哭,有人歡樂(lè)有人憂愁卦溢,有人驚喜有人失落糊余,有的覺(jué)得收獲滿滿有...
    陌忘宇閱讀 8,536評(píng)論 28 53