iOS併發開發簡要整理

在多核處理器沒有大範圍使用開來的時候,就有了多線程的概念,iOS的併發開發也不是新東西了。本篇文章主要是對iOS開發當中經常涉及到的併發內容所做的簡要整理,把學過的用過的東西重新整理到筆頭上。

0. 併發的初衷

記得大學上專業課的時候,老師曾經經常提問一個問題——“併發”vs.“並行”。簡單對比下,並行基本上就是同時進行,而併發不一定保證兩個線程同一時刻在同時作業,而可以體現爲同一個時間段內各自都完成了一部分任務。

在多核上,做的好的併發可以真正的並行,而在單核上,不過是多個線程對處理器計算執行權的交換更替。爲什麼要交替進行?這好比一羣人排隊等着填表做體檢,輪到一個人A的時候,進行了一半發現需要等另一人B到達帶來一些材料,B遲遲不來,那麼在他身後排隊的人C(以及C後面的人)肯定不會就這樣停下,而是更希望A到一邊等B,C和後面的人依次完成任務,知道B來了,A繼續。這樣的好處就是,體檢可以更有效率的進行,總體上縮短這羣人完成體檢的時間,C和C後面的人不用陪着A一起死等B的到來。計算機也是一樣,處理器資源寶貴,如果有現成等待IO完成,其它準備好了的線程可以先來執行。併發既能提高吞吐量,也能讓準備好的線程有機會得到比較好的響應。

在之前我整理Java併發文章的時候也提到過這些。

1. iOS平臺的併發支持

我這裏強調“iOS併發”,是因爲我主要是在iOS開發工作中瞭解到的這些技術,而這些技術並不一定僅侷限於iOS系統。從一定意義上講,iOS是源於蘋果的桌面系統OS X的,所以這裏提到的很多東西也是在整個蘋果開發體系內可用的。

經過了蘋果技術體系的不斷髮展,iOS上對併發的支持從不同層面來看可謂是多種多樣。目前蘋果官方文檔主推的技術是Grand Central Dispatch(GCD)和NSOperation /Operation Queue,這兩個都是使用任務隊列的方式進行併發任務的執行實現。除了這些,Cocoa(Touch)的Foundation裏還有一個NSThread類,這個類相信很多人就比較容易理解的,也很類似java.lang.Thread了,NSObject本身也可以分出NSThread來。在iOS開發當中,C語言的東東也都是可用的,所以像pthread也可以發揮它的作用。

iOS中除了上面提到的這些併發的技術支持外,還有一個概念經常和併發一起出現,Run Loop。後面的文章中,我會整理一下,把我的理解闡述一下。

2. GCD

GCD是蘋果提供的一個簡單、直接的能夠實現併發的方案,需要和block配合使用。在iOS4之後,block的廣泛使用,也讓GCD成爲了一個很好用的工具。之前只用“線程”概念做併發開發的同學,可能不是很容易理解GCD(至少我當初有點迷惑),但如果你做過Java,想想ThreadPoolExecutor就應該明白個大概了。

比較原始的,我們可以創建一個線程,指定它的任務,令其執行。多個任務就開多個線程。但線程不是越多越好,線程會佔用資源,而且線程間的切換也會影響執行效率。所以,我們就要考慮如何維護線程。再結合線程安全,線程同步等問題考慮,線程管理的成本還是很高的。而如果線程的維護管理是系統已經爲我們實現好的,按照一定的策略進行管理調度,那可以大大減輕我們的維護工作,降低成本。所以。無論是iOS上的GCD,還是Java的ThreadPoolExecutor都是這樣做的,我們最後只管往一個隊列裏放入我們想要執行的任務即可。

  • GCD中,一個隊列就是一個 dispatch_queue_t類型的東東,它在GCD中也算是一個Dispatch Object了。隊列從類型上來看分串行的(Serial)和併發的(Concurrent)的,區別在於前者必須等一個任務完成後,下一個任務(block)纔開始進行,後者則不保證。不過兩者都是按順序向隊列裏放任務進去的。此外,蘋果文檔中還單獨給出了一個主隊列(main queue),主隊列其實也是一個串行隊列,與主線程關聯,由main runloop控制,用來處理UI相關的任務。
  • GCD隊列具體怎麼創建和獲取,可參考蘋果文檔,我這裏不會細說。但是需要說明一點的是,主隊列不用自己創建,另外還有全局的(Global)幾個不同優先級的隊列也可以直接獲取到,除非自己需要特殊的串行隊列,這些大多數情況下夠用了。現在串行的隊列和併發的隊列都可以通過 dispatch_queue_create來創建,根據參數區分。但自己創建的要自己維護,負責清理釋放。
  • 向隊列中分派任務,可以以同步的方式進行( dispatch_sync),也可以是異步的( dispatch_async)。同步的方式會在分派任務時阻塞住,直到結束。但考慮可能發生的死鎖情況,同步還是儘量不用的好。除非在能保證安全的情況下,又需要直接拿到同步執行的返回值的。
  • dispatch_once是個很不錯的東西,可以用來實現單例,也主要用來實現單例。Xcode5.1.1中貌似就可以自動完成用dispatch_once實現的代碼塊,很贊很方便。
  • 至於dispatch_apply、dispatch_after這種我就不多說了,很容易懂,也沒有其它幾個常用。dispatch_group則實現了Thread概念下類似Join的功能,等待一個或一組任務完成。
  • GCD任務分派後沒法取消,但可以通過GCD隊列進行suspend和resume控制。而針對併發隊列(Concurrent Queue),可以通過barrier對特定任務進行前後隔離,讀寫鎖實現可考慮。需要注意的是,本段中這幾點,對於Global Queue是無效的!
  • GCD隊列可組合使用。在GCD隊列中的任務出隊列時不一定就執行了,也可能進到另一個隊列。而Global隊列有優先級之分,因此可以通過隊列組合指定優先級,方法就是dispatch_set_target_queue()。
GCD隊列圖

GCD隊列圖(來源於Objc中國)

  • 對於阻塞的調用,儘管脫離開主線程,使得App的UI不受影響,但特定線程的資源還是會得不到充分利用,可考慮使用dispatch source作異步解決方案。

GCD簡單易用,是併發開發很好上手的工具,這裏就把主要的內容整理至此。

除了上面提到的這些,GCD還有很多其它內容,還有信號量(semaphore)、dispatch data、dispatch io等。除了參考蘋果的《Concurrency Programming Guide》所述,《GCD Reference》東西更全。

3. Operation Queue

說完了GCD,Operation Queue理解起來就簡單多了。可以說,Operation Queue是在語言上Objective-C的一個包裝,實際上也是這樣,最終的實現可以認爲還是利用了GCD。

既然是包裝了GCD,那一定多做了一些事情,最基本的就是面向對象,是Objective-C中NSObject的子類對象,可繼承擴展,可封裝複用。除此之外,Operation支持KVO、支持優先級、支持completion block,但我最關注的還是另外兩點:

  • 支持cancel
  • 支持任務間依賴(注意,依賴高於優先級priority)

有了這兩點支持,在某些複雜的業務場景下,用Operation比用GCD省事很多。

按照蘋果官方文檔所述,NSOperation是“抽象”的(雖然Objective-C中沒有真正意義上的抽象概念),需要繼承、擴展實現,方可使用。當然,蘋果還是給了兩個可用的子類,NSBlockOperation和NSInvocationOperation。這些怎麼用蘋果文檔裏寫得都很清楚,注意NSBlockOperatioan裏的block是併發的。

對於自己擴展實現的Operation,最主要的最基本的,把main方法實現了。做好異常處理是好習慣。

關於Operation的執行,既可以放到Operation Queue裏(通常是這麼用),也可以單獨start。放到NSOperationQueue對象裏會增加operation的引用計數,同時也保證了異步執行。而如果是調用start進行執行就不一定了,能否異步全看這個Operation是如何實現的。Operation的實現類別分爲同步和併發的,可通過isConcurrent區別。默認情況下,這個方法返回NO,意味着調用start方法單獨執行時都是在當前線程執行的。如果需要併發,就要負責自己對Operation的併發特性進行實現:

  • 首先,不能再用父類的start方法實現,需要自己負責開啓新線程執行自定義的任務。
  • isConcurrent方法自然要修改爲返回YES。
  • 同時,對isFinished和isExecuting等方法需要按新的自定義邏輯進行實現,同時保持對KVO的兼容。

關於Operation的取消執行支持,這個是之前提到的Operation的一個特點。而這也是需要開發者參與定義的,在執行構成中的特定點判斷是否需要終止執行。需要注意的一點是,被cancel掉的Operation同樣也被認爲是finished狀態的,這意味着一個依賴於其它Operation的Operation,只要它的依賴全部都cancel了,也可以開始start執行了。此外,也可以對一個Operation Queue對象直接做取消操作,取消隊列中所有任務。

4. 其它

不管是GCD還是Operation Queue,都沒有對傳統線程(Thread)的直接使用,對線程模型進行了包裝,避免了一些比較繁瑣的問題,降低了對線程對象的維護成本。那麼想用好這兩種工具,我們需要做的就是對要處理的任務進行合理抽象拆分,交給合適的隊列處理。和線程要佔用系統資源一樣,NSOperation對象也要恰當使用,不是越多越好,要控制在合適的範圍。

就像前文提併發的初衷所說,併發是爲了更合理更有效率的利用計算資源,而較慢的IO處理是一個阻力。併發和IO通常是緊密關聯的兩個話題。在實際開發中,即使使用GCD和NSOperation對任務做了併發異步處理,也儘量不要讓任務出現阻塞,因爲GCD和NSOperation底層也是依賴線程實現的,一個阻塞的動作也會使一個線程對應的資源變得浪費。對於這種情況,我們儘量不要佔用線程,儘量利用系統所提供的各項異步IO方案。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章