承接上一篇,輕鬆無痛實現非同步操作序列。 如果沒看過上一篇,閱讀本篇可能會有點懵逼。
在上一篇文章中,我主要描述瞭如何實現非同步序列運算子,+>
。並演示瞭如何基於他來做一些諸如引數的傳遞和錯誤的處理等操作。
這篇文章中,我們會基於之前的發現,來實現非同步並行運算子 。 以及基於
+>
和 來做一些有趣的應用。
本文的主要內容:
- 實現並行摺疊運算子:
;
- 基於
+>
和,實現一個簡潔優雅的 Promise 介面;
第一部分 能夠摺疊非同步並行操作的運算子
什麼是摺疊
首先,我們需要定義什麼是非同步並行? 就是我們同時執行多個非同步操作,當所有操作都執行完畢後,執行非同步(Complete)回撥。比如我們已經有了使用者的 ID,需要同時請求使用者的頭像和基本資料。在兩個請求都拿到資料時,重新整理介面。
在上一篇文章中,我們在提出運算子 +>
之前,提出了一個連線的概念。指的是把兩個非同步操作連線起來,一個執行完就執行另一個。通過連線,把兩個非同步操作合併為一個。
但現在非同步並行,顯然不能用連線,因為多個請求是一起發生的,沒有先後順序。在本文中,用摺疊來表示把多個非同步請求以並行的方式合併為一個的過程。
基本分析
首先,回憶一下我們非同步序列運算子的簽名:
1 2 3 |
typealias AsyncFunc = (info : AnyObject,complete:(AnyObject?,NSError?)->Void) -> Void +> : (AsyncFunc,AsyncFunc) -> AsyncFunc |
我們通過實現把兩個非同步操作摺疊為一個,來實現序列摺疊任意多個非同步操作。
並行的思路也是一樣的,我們只要實現並行摺疊兩個非同步操作,我們就能摺疊任意多個非同步操作。
我們首先寫出函式的簽名:
1 |
func (left : AsyncFunc, right : AsyncFunc) -> AsyncFunc |
為什麼我們選擇的序列非同步運算子
+>
是非對稱的,而並行非同步運算子卻是對稱的呢?這還是由序列非同步和並行非同步兩個運算的性質決定的,序列非同步不滿足交換律,因為序列就代表了運算本身有先後。而並行卻沒這個限制。
a b == b a
,但a +> b != b +> a
按照慣例,我們先根據函式的簽名(返回一個函式),擼個基本的架子:
1 2 3 4 5 |
func (left : AsyncFunc , right : AsyncFunc) -> AsyncFunc{ return { info, complete in } } |
架子搭好以後,我們來思考一下如何實現函式體, 有以下幾個方面
這裡的函式體,是指我們
return
後面的函式的函式體,而不是的函式體,如果一味思考後者,很容易懵逼。函數語言程式設計的一個關鍵技巧就是通過型別來拆分抽象層次,區域性具體,總體抽象。
- 主體邏輯
既然我們的是用來把兩個非同步操作並行摺疊成一個,所以我們返回的函式體要實現的功能就是同時執行
left
和right
這兩個函式,當兩個函式都執行完畢後(兩者都呼叫了自己的 complete 閉包),再呼叫最外層的 complete 閉包,也就是我們返回的函式簽名的第二個引數。
- 引數傳遞
最外層的引數info
, 代表總的輸入引數。需要分別在呼叫left
和right
時傳給它們。那如何表達並行摺疊後的非同步呼叫的結果呢?我們知道left
和right
作為型別為AsyncFunc
的非同步函式,在它們呼叫自己的complete
閉包時都會帶上自己的結果。其中一種可選的方式就是把left
和right
的結果通過陣列合並,當做摺疊後的非同步的結果。
實現非同步摺疊運算子
基於以上的分析,我們大概可以給出如下的實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
func (left : AsyncFunc , right : AsyncFunc) -> AsyncFunc{ return { info, complete in var leftComplete = false var rightComplete = false var leftResult:AnyObject? = nil var rightResult:AnyObject? = nil let checkComplete = { if leftComplete && rightComplete{ let finalResult:[AnyObject] = [leftResult!, rightResult!] complete(finalResult, nil) } } left(info: info){result,error in guard error == nil else{ complete(nil, error) return } leftComplete = true leftResult = result; checkComplete() } right(info: info){result,error in guard error == nil else{ complete(nil, error) return } rightComplete = true rightResult = result; checkComplete() } } } |
上面的程式碼邏輯其實很簡單,我們通過一個 checkComplete
函式來檢查兩個任務是否都已經完成,如果完成則合併兩個非同步函式返回的結果,並呼叫最外層的 complete
閉包。 兩個非同步函式則直接呼叫,在 complete
閉包中檢查是否出錯,沒有則儲存相應的結果,和置對應的標誌位。
測試一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
let delay = dispatch_time(DISPATCH_TIME_NOW, Int64(NSEC_PER_SEC)) let test1:AsyncFunc = { _,complete in print("test1") dispatch_after(delay, dispatch_get_main_queue(), { complete(0,nil); }) } let test2:AsyncFunc = { _,complete in print("test2") dispatch_after(delay, dispatch_get_main_queue(), { complete(0,nil); }) } let test = test1 test2; test(info: 0){ _,_ in print("all finished")}; |
上述程式碼中,我們建立了兩個非同步操作:test1
和 test2
。 然後通過我們的並行摺疊運算子 摺疊為一個:
test
。之後直接執行 test。
結果輸出:
1 2 3 |
test1 test2 all finished |
我們執行摺疊後的函式,test1
和 test2
都得到了呼叫,並且在都完成之後,呼叫了最外層的 complete
閉包:列印出了 all finished
。看上去很完美。
精益求精
但是真的完美了嗎?
在上述測試程式碼中,我們把 main_queue
換成 global_queue
之後,我們會發現最外層的 complete
閉包被執行了兩次,最終列印了兩次 all finished
, 這明顯不是我們想要的結果。
上面的程式碼其實會有一個經典的多執行緒問題,如果 left
和 right
的 complete
閉包是併發呼叫的話,就有可能在執行完 leftComplete = true
的時候執行被切走,執行 right
的 complete
閉包,執行完 right
之後繼續 left
這邊的執行。這個時序就會導致最終被執行兩次。
解決也很簡單,我們只要加一個變數來當做互斥鎖即可,最終的並行摺疊運算子修改如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
func (left : AsyncFunc , right : AsyncFunc) -> AsyncFunc{ return { info, complete in var leftComplete = false var rightComplete = false var finishedComplete = false var leftResult:AnyObject? = nil var rightResult:AnyObject? = nil let checkComplete = { if leftComplete && rightComplete{ objc_sync_enter(finishedComplete) if !finishedComplete{ let finalResult:[AnyObject] = [leftResult!, rightResult!] complete(finalResult, nil) finishedComplete = true } objc_sync_exit(finishedComplete) } } left(info: info){result,error in guard error == nil else{ complete(nil, error) return } leftComplete = true leftResult = result; checkComplete() } right(info: info){result,error in guard error == nil else{ complete(nil, error) return } rightComplete = true rightResult = result; checkComplete() } } } |
至此,我們擁有了一個優雅的並行摺疊運算子:, 和
+>
一樣。可以幫助我們簡化程式碼,抽象邏輯。 當然,閒的蛋疼要對其玩一玩map/filter/reduce
之類也是支援的,和上篇介紹的思路一樣。在此不再贅述。
第二部分,100行實現類 PromiseKit 的介面
鏡頭切換到一些實際應用的場景,很多時候我們傾向於通過 closure
來組織邏輯,這樣可以把本身就耦合的邏輯寫在一個地方,也更容易維護。我們的並行摺疊和序列連線運算子都是基於函式的,能不能應用在 closure based scenario
呢? let try it.
考慮介面易用性,我們 API 的設計可以直接參(shan)考(zhai) PromiseKit.
PromiseKit
GitHub主頁的 Readme 給了這樣的一個例子:
1 2 3 4 5 6 7 8 9 10 |
firstly { when(NSURLSession.GET(url).asImage(), CLLocationManager.promise()) }.then { image, location -> Void in self.imageView.image = image self.label.text = "\(location)" }.always { UIApplication.sharedApplication().networkActivityIndicatorVisible = false }.error { error in UIAlertView(/*…*/).show() } |
我們來分析一下他都做了什麼:
僅從 API 字面分析,本文不涉及 PromiseKit 內部真正的實現機制
- 通過
firstly
註冊第一個任務,並返回一個 Promise 物件。用於後面的鏈式程式碼書寫。 when
函式接受兩個同步的任務,同時觸發兩個任務並阻塞當前的執行,直到兩個任務都完成。(非同步並行的場景),這裡雖然when
會阻塞執行,但when
本身是執行在主執行緒中的,也不會阻塞主執行緒。then
可以有任意多個,順序執行。then
塊中直接用同步的方式寫程式碼。但最終這些任務都會被非同步的執行。(非同步序列的場景)- 不管執行過程中是否出錯,都會執行
always
塊 - 如果執行過程中出錯,則執行
error
塊。
基於以上的分析,我們一步步來實現這幾個元件:
firstly
firstly
用於接收第一個任務,任務書寫是同步的方式,但必須非同步執行。
1 2 3 4 5 6 7 8 9 10 |
func firstly(body : Void->Void)->Promise{ let starter: AsyncFunc = { _,complete in dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0)) { body(); complete(0,nil); } } return Promise(starter: starter) } |
我們的 firstly
實現只做了兩件事, 把第一個任務包成非同步的,並用這個任務建立了一個 Promise
物件並返回。
因為所有任務最終都是由
Promise
物件來維護的,所以firstly
只需要把第一個任務直接給他即可。
Promise 類基礎
根據之前的分析,我們先把顯而易見的架子擼出來:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
class Promise { var chain : AsyncFunc var alwaysClosure : (Void->Void)? var errorClosure : (NSError?->Void)? init(starter : AsyncFunc){ chain = starter } func then(body : AnyObject throws->Void )->Promise{ //TO BE IMP return self } func always(closure : Void->Void)->Promise{ alwaysClosure = closure return self } func error(closure : NSError?->Void)->Promise{ errorClosure = closure fire() return self } func fire(){ chain(info: 0) { (info, error) in if let always = self.alwaysClosure{ always() } if error == nil{ print("all task finished") }else{ if let errorC = self.errorClosure{ errorC(error) } } } } } |
上述程式碼實現了除 then
函式之外的所有部件。我們把初始任務存在成員 chain
上面,然後分別用成員儲存 error closure
和 always closure
, 然後在註冊完 error closure
之後呼叫 fire
來觸發 chain
的執行,在 chain
執行完畢後分別執行 always
和是否出錯來執行 error
.
then
,always
,error
都返回self
, 實現鏈式呼叫。
至此,我們已經實現了能執行一個任務,並且實現 always
和 error
機制的 Promise
物件。
無限的、鏈式 then 塊。
如之前所說,我們把 firstly
傳進來的初始任務儲存在 chain
這個成員中。那之後的 then
傳入的其實就是後續的任務,比如有三個鏈式的 then
,就代表我們需要序列的執行四個任務:初始任務,三個 then
塊的任務。
所以,我們的 then 函式可以這樣來實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
let async: AsyncFunc = { info, complete in dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0)) { var error : NSError? do{ try body(info) }catch let err as NSError{ error = err } complete(0,error) } } chain = chain +> async return self } |
顯而易見, then
做的事情和 firstly
並無太多區別,首先把傳進來的同步任務打包成非同步,第二步是把新的任務通過非同步序列運算子 +>
合併到成員 chain
中。這樣,chain
儲存的就不僅僅是初始任務,而是像一個累加器一樣,有多少 then
, chain
就是最終合併的任務。這樣,我們不管 then
多少次,每個 then
塊中的任務都會被合併到 chain
裡。最終我們只需要執行 chain
, 即可觸發所有任務的鏈式執行(因為合併用的是 +>
)。
注意在
then
塊中執行body
的時候用了do-catch
結構,目的就是在then
塊接受的任務可以通過throw
丟擲錯誤,然後在這裡捕獲,實現錯誤的感知(如果捕獲到錯誤,則最終會呼叫errorClosure
)
實現 when 函式
我們溫習一下上文對 when
函式的分析:
when
函式接受兩個同步的任務,同時觸發兩個任務並阻塞當前的執行,直到兩個任務都完成。(非同步並行的場景),這裡雖然when
會阻塞執行,但when
本身是執行在主執行緒中的,也不會阻塞主執行緒。
根據 when
函式的定位,只要簡單實現成獨立的函式即可,不需要實現為 Promise
類的成員。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
func when(fstBody : (Void->Void), sndBody : (Void->Void)){ let async1 : AsyncFunc = { _ , complete in dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0)) { fstBody(); complete(0,nil); } } let async2 : AsyncFunc = { _ , complete in dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0)) { sndBody(); complete(0,nil); } } let async = async1 async2 var finished = false async(info: 0) { (_, _) in finished = true } while finished == false { } } |
上述程式碼中,when
首先把傳入的兩個同步任務打包成一部,並通過非同步並行運算子 合併,然後直接執行合併後的結果。合併後的結果回撥時(也就是兩個任務都完成時),置
finished
為 true
。 末尾用一個 while
在 finished
為 false
時阻塞函式的執行。
至此,我們完成了一個最簡單的 Promise
的封裝, firstly
、 Promise 主類
和when
三個元件,加起來一共100行
老規矩,來測試一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
firstly { () in when({ () in print(“begin fst job") sleep(1) print("fst job in when finished") }, sndBody: { () in print(“begin snd job") sleep(5) print("snd job in when finished") }) }.then { (info) in print("second job") }.then { (info) in print("third job") }.always { () in print("always block") }.error { (error) in print("error occurred") } |
執行流程:同時執行 when
的兩個任務,都完成之後按順序執行 then
, 最後執行 always
。因為過程中沒有error,所以 error
塊沒有被呼叫。
1 2 3 4 5 6 7 8 |
begin fst job begin snd job (間隔1秒)fst job in when finished (間隔4秒) snd job in when finished second job third job always block |
現在來簡單修改一下程式碼,在 second job
裡丟擲一個 error:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
firstly { () in when({ () in print(“begin fst job") sleep(1) print("fst job in when finished") }, sndBody: { () in print(“begin snd job") sleep(5) print("snd job in when finished") }) }.then { (info) in print("second job") throw NSError(domain: "error", code: 0, userInfo: [:]) }.then { (info) in print("third job") }.always { () in print("always block") }.error { (error) in print("error occurred") } |
最終輸出:
1 2 3 4 5 6 7 8 |
begin fst job begin snd job (間隔1秒)fst job in when finished (間隔4秒) snd job in when finished second job always block error occurred |
對比之前的結果,因為丟擲了錯誤,所以 error 塊得以執行,並且thrid job
沒有執行,因為出錯中斷了 then
鏈的執行。
總結
- 上一篇文章中,我們實現了非同步序列運算子:
+>
; - 本篇文章中,我們首先實現了非同步並行運算子:
;
- 然後,基於上面兩個運算子,我們用100行實現了一個簡單的 Promise 實現;