GCD和Operation/OperationQueue 看這一篇文章就夠了
Grand Central Dispatch簡稱GCD,是蘋果公司為多核的並行運算提出的解決方案, 允許程序將任務切分為多個單一任務然後提交至工作隊列來並發地或者串列地執行。GCD會自動利用更多的CPU內核(比如雙核、四核), 自動管理線程的生命周期(創建線程、調度任務、銷毀線程)。
下面逐一介紹DispatchQueue, Operation和OperationQueue.
1. DispatchQueue
GCD的基本概念就是DispatchQueue。DispatchQueue是一個對象,它可以接受任務,並將任務以FIFO(先到先執行)的順序來執行。DispatchQueue可以是並發的或串列的, 它有3種隊列類型:
Main queue
Global queue
Custom queue
1.1 Main queue(串列Serial)
Main queue運行在系統主線程.它是一個串列隊列,我們所有的UI刷新都發生在Main queue.獲取Main queue的方法很簡單:
如果要在非Main queue線程中直接刷新UI, 運行時會出exception. 一般的做法是將代碼放在Main queue中非同步執行:
1.2 Global queues(並行Concurrent)
如果要在後台執行非UI相關的工作, 一般把這部分工作放在Global queue. Global queue是一種系統內共享的並行的隊列. 申請Global queue的方法很簡單:
Global queue隊列有四種優先順序: 高, 預設, 低, 後台. 在實際申請Global queue時,我們不需直接指定優先順序, 只需申明所需的(QoS)類型, Qos間接的決定了這些queue的優先順序
例如:
The QoS classes are:
User-interactive
表示為了給用戶提供一個比較好的體驗, 任務必須立即完成. 主要用於UI刷新, 低延遲的事件處理等. 在整個App內, 這種類型的任務不宜太多. 它是最高優先順序的.
User-initiated
用於UI發起非同步任務,用戶在等待執行的結果, 這種queue是高優先順序的.
Utility
長時間運行的任務, 典型情況是App中會有一個進度條表示任務的進度. 主要用於 計算, I/O, 網路交互等, 主要為節能考慮. 這種queue是低優先順序的.
Background
任務在運行, 但用戶感覺不到它在運行的場景. 主要用於不需要用戶干涉,對時間不敏感的獲取數據等任務, 這種queue是後台優先順序,屬於最低優先順序的那一種.
1.3 Custom queues
用戶創建的Custom queues默認的是串列的, 如果指定了attributes為concurrent則為並行的. 下面我們用代碼演示串列隊列/並行隊列的區別.
Serial Queue
除了DispatchQueue.main是並行的queue外, 你也可以創建自己的並行queue(預設為串列)
上面代碼的輸出為:
從上可以看出, task1和task2是順序運行的, 只有task1執行完了後task2才可以執行.由於task1和task2都是非同步(asyc)執行的, 所以不會阻塞當前線程, 2個任務執行的時間只有0.00026秒.
Concurrent Queue
創建用戶自己的並行queue, 聲明.concurrent屬性即可:
上面的輸出為:
從上面可以看出, task1和task2是並發執行的, task1啟動後, 由於執行時間需要1s, 這個時候task2也可以同步運行, 所以我們可以看到task1啟動後, 立即啟動task2, 然後task2完成, task1完成.
task1和task2都是非同步(async)運行的,所以它們花費的時間仍然很短, 只有0.00034秒.
同步(Synchronous) vs. 非同步(Asynchronous)
對於一個任務(function), 可以在GCD隊列里同步運行, 也可以非同步運行。
同步運行的任務, 不開啟新的線程, 會阻塞當前線程, 等任務完成才返回。
非同步運行的任務, 會開啟新的線程, 不會阻塞當前線程, 分發任務後立即返回,不用等任務完成。
2. DispatchGroup
DispatchGroup實例用來追蹤不同隊列中的不同任務。當group里所有事件都完成GCD API有兩種方式發送通知,第一種是DispatchGroup.wait,會阻塞當前進程,等所有任務都完成或等待超時。第二種方法是使用DispatchGroup.notify,非同步執行閉包,不會阻塞當前線程。
以上程序的輸出如下:
3. Operation/OperationQueue
GCD是一個底層的C API, OperationQueue是基於GCD和隊列模型的一個抽象,負責Operation的調度.這意味著我們可以像GCD那樣並行的執行任務, 但是以面向對象的方式.
GCD和OperationQueue主要差別如下:
OperationQueue不遵循FIFO: 相比於GCD, OperationQueue使用起來更加靈活. 在OperationQueue中,我們可以這是Operation之間的依賴, 例如Operation-B任務只有在Operation-A執行完成之後才能開始執行. 這也是OperationQueue不遵循FIFO的原因.
OperationQueue並行運行: OperationQueue中的任務(Operation)並行執行, 你不能設置成串列Serial. 但是可以有個變通方法達到串列運行的效果,就是設置依賴, C依賴B, B依賴A; 所以他們運行的順序就是 A -> B -> C
一個獨立的Operation任務是同步運行的, 如果想讓Operation非同步運行, 你必須將Operation加入到一個OperationQueue.
Operation queues 是OperationQueue的一個實例, 實際上Operation queue是被封裝在一個Operation中運行的.
3.1 Operation
提交到OperationQueue中的任務必須是一個Operation實例.你可以簡單的認為Operaion就是一項工作/任務. Operation是一個不能直接使用的抽象類,在實際使用中你必須用Operation的子類.
在iOS SDK中, 蘋果提供了2種Operation的可實例化子類BlockOperation 和 InvocationOperation, 這2種類我們可以直接使用.
BlockOperation. 對DispatchQueue.global()的一個封裝, 它可以管理一個或多個blocks, bocks非同步,並行的執行, 如果你想串列的執行任務, 你可以設置依賴或選擇Custom queues(參見DispatchQueue).BlockOperation為一些已經使用了OperationQueue但不想使用DispatchQueue的App, 提供一種面向對象的封裝. 作為一種Operation, 相比DisptachQueue, 它提供了更多的特性例如添加依賴, KVO通知, 取消任務等. 某種程度上BlockOperation也像一個一個DispatchGroup: 所有的blocks完成後它會收到通知.
如果BlockOperation中只有一個任務, 那麼這個任務會在當前線程中. 如果有多個任務, 那麼系統可能會開啟多個線程來執行這些任務
The output is as fellows:
加入到blockOperation的每個block需要花費2s, 從輸出日誌可以看出, 10個blocks運行完花費了約4s. 說明這些block是並行運行的,至少有5個線程同時運行.
InvocationOperation– 已經從Swift中移除了,無需關注
關於自定義的Operation, 下面以一個例子介紹該如何使用. 這個例子是一個Operation, 將我女朋友的照片模糊化後輸出.
首先定義一個類,繼承與Operation:
其中blurImage(image: inputImage)是將輸入的圖片模糊化, 它是一個耗時任務.
然後使用這個BlurImageOperation:
注意Operation啟動的方式有2中, 要麼手動.start()啟動,要麼將Operation加入到OperationQueue中自動啟動.這裡我們沒用OperationQueue,所以要手動啟動.
Operation本身是同步執行的, 所以operation.start()會阻塞在這裡, 可能會花較長時間. operation執行完成後, 在Playground中點擊operation.outputImage對應的側邊欄上的小眼睛Quick Look可以查看處理完成的圖片:
從日誌輸出可以看出, 總的圖片處理BlurImageOperation花費了4.9s
3.2 OperationQueue
OperationQueue負責Operation任務集的調度, Operation加入OperationQueue後, 立即啟動運行, 無需手工啟動. 通過maxConcurrentOperationCount設置並發任務的數量:
下面一個具體的例子來說明OperationQueue的用法.
這個例子中, 申明了一個 printerQueue, 然後往裡添加了6個任務Operation, 每個Operation輸出一個字後sleep 3秒. 執行結果如下:
可以看出, 每個Operation都要花3秒, 而printerQueue實際也只花費了3秒, 說明這些Operation都是並行執行的.
輸出的順序雖然和Operation加入的順序是一樣的,這其實是一種巧合, 實際情況不一定是這樣的. 如果你將maxConcurrentOperationCount 設置為2(取消上面的注釋行),
你會發現輸出是這樣的:
每個Operation花費3秒, 最大並發數為2, 兩兩一組可分為3組, 6個Operation總共花費了3*3 = 9秒, 輸出順序和加入Operation的順序不完全一樣,進一步說明加入OperationQueue的Operation是concurrent執行的.
如果大家感興趣, 下一篇我可以介紹下Operation依賴的使用, 以及如何避免多線程的data race問題.


※李笑來:區塊鏈技術終將改變世界,長期樂觀,短期保守
※幾次面試後,我的一些思考和總結
TAG:Cocoa開發者社區 |