【Swift 腦洞系列】並行非同步運算以及100行的 PromiseKit

發表於2016-08-31

承接上一篇,輕鬆無痛實現非同步操作序列。 如果沒看過上一篇,閱讀本篇可能會有點懵逼。

在上一篇文章中,我主要描述瞭如何實現非同步序列運算子,+>。並演示瞭如何基於他來做一些諸如引數的傳遞和錯誤的處理等操作。

這篇文章中,我們會基於之前的發現,來實現非同步並行運算子 。 以及基於 +>來做一些有趣的應用。

本文的主要內容:

  • 實現並行摺疊運算子:
  • 基於 +>,實現一個簡潔優雅的 Promise 介面;

第一部分 能夠摺疊非同步並行操作的運算子

什麼是摺疊

首先,我們需要定義什麼是非同步並行? 就是我們同時執行多個非同步操作,當所有操作都執行完畢後,執行非同步(Complete)回撥。比如我們已經有了使用者的 ID,需要同時請求使用者的頭像和基本資料。在兩個請求都拿到資料時,重新整理介面。

在上一篇文章中,我們在提出運算子 +> 之前,提出了一個連線的概念。指的是把兩個非同步操作連線起來,一個執行完就執行另一個。通過連線,把兩個非同步操作合併為一個。

但現在非同步並行,顯然不能用連線,因為多個請求是一起發生的,沒有先後順序。在本文中,用摺疊來表示把多個非同步請求以並行的方式合併為一個的過程。

基本分析

首先,回憶一下我們非同步序列運算子的簽名:

我們通過實現把兩個非同步操作摺疊為一個,來實現序列摺疊任意多個非同步操作。

並行的思路也是一樣的,我們只要實現並行摺疊兩個非同步操作,我們就能摺疊任意多個非同步操作。

我們首先寫出函式的簽名:

為什麼我們選擇的序列非同步運算子 +> 是非對稱的,而並行非同步運算子 卻是對稱的呢?這還是由序列非同步和並行非同步兩個運算的性質決定的,序列非同步不滿足交換律,因為序列就代表了運算本身有先後。而並行卻沒這個限制。a b == b a ,但 a +> b != b +> a

按照慣例,我們先根據函式的簽名(返回一個函式),擼個基本的架子:

架子搭好以後,我們來思考一下如何實現函式體, 有以下幾個方面

這裡的函式體,是指我們 return 後面的函式的函式體,而不是 的函式體,如果一味思考後者,很容易懵逼。函數語言程式設計的一個關鍵技巧就是通過型別來拆分抽象層次,區域性具體,總體抽象。

  • 主體邏輯
    既然我們的 是用來把兩個非同步操作並行摺疊成一個,所以我們返回的函式體要實現的功能就是同時執行 leftright 這兩個函式,當兩個函式都執行完畢後(兩者都呼叫了自己的 complete 閉包),再呼叫最外層的 complete 閉包,也就是我們返回的函式簽名的第二個引數
  • 引數傳遞
    最外層的引數 info, 代表總的輸入引數。需要分別在呼叫 leftright 時傳給它們。那如何表達並行摺疊後的非同步呼叫的結果呢?我們知道 leftright 作為型別為 AsyncFunc 的非同步函式,在它們呼叫自己的 complete 閉包時都會帶上自己的結果。其中一種可選的方式就是把 leftright 的結果通過陣列合並,當做摺疊後的非同步的結果。

實現非同步摺疊運算子

基於以上的分析,我們大概可以給出如下的實現:

上面的程式碼邏輯其實很簡單,我們通過一個 checkComplete 函式來檢查兩個任務是否都已經完成,如果完成則合併兩個非同步函式返回的結果,並呼叫最外層的 complete 閉包。 兩個非同步函式則直接呼叫,在 complete 閉包中檢查是否出錯,沒有則儲存相應的結果,和置對應的標誌位。

測試一下

上述程式碼中,我們建立了兩個非同步操作:test1test2。 然後通過我們的並行摺疊運算子 摺疊為一個: test。之後直接執行 test。

結果輸出:

我們執行摺疊後的函式,test1test2 都得到了呼叫,並且在都完成之後,呼叫了最外層的 complete 閉包:列印出了 all finished。看上去很完美。

精益求精

但是真的完美了嗎?

在上述測試程式碼中,我們把 main_queue 換成 global_queue之後,我們會發現最外層的 complete 閉包被執行了兩次,最終列印了兩次 all finished, 這明顯不是我們想要的結果。

上面的程式碼其實會有一個經典的多執行緒問題,如果 leftrightcomplete 閉包是併發呼叫的話,就有可能在執行完 leftComplete = true 的時候執行被切走,執行 rightcomplete 閉包,執行完 right 之後繼續 left 這邊的執行。這個時序就會導致最終被執行兩次。

解決也很簡單,我們只要加一個變數來當做互斥鎖即可,最終的並行摺疊運算子修改如下:

至此,我們擁有了一個優雅的並行摺疊運算子:, 和 +> 一樣。可以幫助我們簡化程式碼,抽象邏輯。 當然,閒的蛋疼要對其玩一玩map/filter/reduce之類也是支援的,和上篇介紹的思路一樣。在此不再贅述。

第二部分,100行實現類 PromiseKit 的介面

鏡頭切換到一些實際應用的場景,很多時候我們傾向於通過 closure 來組織邏輯,這樣可以把本身就耦合的邏輯寫在一個地方,也更容易維護。我們的並行摺疊和序列連線運算子都是基於函式的,能不能應用在 closure based scenario 呢? let try it.

考慮介面易用性,我們 API 的設計可以直接參(shan)考(zhai) PromiseKit.

PromiseKit GitHub主頁的 Readme 給了這樣的一個例子:

我們來分析一下他都做了什麼:

僅從 API 字面分析,本文不涉及 PromiseKit 內部真正的實現機制

  • 通過 firstly 註冊第一個任務,並返回一個 Promise 物件。用於後面的鏈式程式碼書寫。
  • when 函式接受兩個同步的任務,同時觸發兩個任務並阻塞當前的執行,直到兩個任務都完成。(非同步並行的場景),這裡雖然 when 會阻塞執行,但 when 本身是執行在主執行緒中的,也不會阻塞主執行緒。
  • then 可以有任意多個,順序執行。then 塊中直接用同步的方式寫程式碼。但最終這些任務都會被非同步的執行。(非同步序列的場景
  • 不管執行過程中是否出錯,都會執行 always
  • 如果執行過程中出錯,則執行 error 塊。

基於以上的分析,我們一步步來實現這幾個元件:

firstly

firstly用於接收第一個任務,任務書寫是同步的方式,但必須非同步執行。

我們的 firstly 實現只做了兩件事, 把第一個任務包成非同步的,並用這個任務建立了一個 Promise 物件並返回。

因為所有任務最終都是由 Promise 物件來維護的,所以 firstly 只需要把第一個任務直接給他即可。

Promise 類基礎

根據之前的分析,我們先把顯而易見的架子擼出來:

上述程式碼實現了除 then 函式之外的所有部件。我們把初始任務存在成員 chain 上面,然後分別用成員儲存 error closurealways closure, 然後在註冊完 error closure 之後呼叫 fire 來觸發 chain 的執行,在 chain 執行完畢後分別執行 always 和是否出錯來執行 error.

then, always, error 都返回 self, 實現鏈式呼叫。

至此,我們已經實現了能執行一個任務,並且實現 alwayserror 機制的 Promise 物件。

無限的、鏈式 then 塊。

如之前所說,我們把 firstly 傳進來的初始任務儲存在 chain 這個成員中。那之後的 then 傳入的其實就是後續的任務,比如有三個鏈式的 then,就代表我們需要序列的執行四個任務:初始任務,三個 then塊的任務。

所以,我們的 then 函式可以這樣來實現:

顯而易見, then 做的事情和 firstly並無太多區別,首先把傳進來的同步任務打包成非同步,第二步是把新的任務通過非同步序列運算子 +> 合併到成員 chain 中。這樣,chain 儲存的就不僅僅是初始任務,而是像一個累加器一樣,有多少 then, chain就是最終合併的任務。這樣,我們不管 then 多少次,每個 then 塊中的任務都會被合併到 chain 裡。最終我們只需要執行 chain, 即可觸發所有任務的鏈式執行(因為合併用的是 +>)。

注意在 then 塊中執行 body 的時候用了 do-catch 結構,目的就是在 then 塊接受的任務可以通過 throw 丟擲錯誤,然後在這裡捕獲,實現錯誤的感知(如果捕獲到錯誤,則最終會呼叫 errorClosure

實現 when 函式

我們溫習一下上文對 when 函式的分析:

when 函式接受兩個同步的任務,同時觸發兩個任務並阻塞當前的執行,直到兩個任務都完成。(非同步並行的場景),這裡雖然 when 會阻塞執行,但 when 本身是執行在主執行緒中的,也不會阻塞主執行緒。

根據 when 函式的定位,只要簡單實現成獨立的函式即可,不需要實現為 Promise類的成員。

上述程式碼中,when 首先把傳入的兩個同步任務打包成一部,並通過非同步並行運算子 合併,然後直接執行合併後的結果。合併後的結果回撥時(也就是兩個任務都完成時),置 finishedtrue。 末尾用一個 whilefinishedfalse 時阻塞函式的執行。

至此,我們完成了一個最簡單的 Promise 的封裝, firstlyPromise 主類when 三個元件,加起來一共100行

老規矩,來測試一下

執行流程:同時執行 when 的兩個任務,都完成之後按順序執行 then, 最後執行 always。因為過程中沒有error,所以 error 塊沒有被呼叫。

現在來簡單修改一下程式碼,在 second job 裡丟擲一個 error:

最終輸出:

對比之前的結果,因為丟擲了錯誤,所以 error 塊得以執行,並且thrid job 沒有執行,因為出錯中斷了 then 鏈的執行。

總結

  • 上一篇文章中,我們實現了非同步序列運算子: +>
  • 本篇文章中,我們首先實現了非同步並行運算子:
  • 然後,基於上面兩個運算子,我們用100行實現了一個簡單的 Promise 實現;

本文所有程式碼: https://github.com/aaaron7/functional_async_demo

相關文章