在初學iOS相關知識過程當中,大多都對多線程有些恐懼的內心,同時感受工做中用上的機率不大。可是若是平時很少積累並學透多線程,當工做中真的須要用到的時候,就極可能簡單百度後把一些知識點稀裏糊塗地就用到工做中了,卻不知裏面有不少的坑,也有不少技巧須要在理論上先作了解,再結合實戰,進一步去體會多線程的魅力和強大。html
接下來,就對多線程來源的背景進行簡單的介紹:ios
在計算的早期,計算機能夠執行的最大工做量是由 CPU 的時鐘速度決定的。可是隨着技術的進步和處理器設計的緊湊化,熱量和其餘物理約束開始限制處理器的最大時鐘速度。所以,芯片製造商尋找其餘方法來提升芯片的整體性能。他們決定的解決方案是增長每一個芯片上的處理器核心數量。經過增長內核的數量,一個單獨的芯片能夠每秒執行更多的指令,而不用增長 CPU 的速度或改變芯片的大小或熱特性。惟一的問題是如何利用額外的內核。git
應用程序使用多核的傳統方法是建立多個線程。與依賴線程不一樣,iOS 採用異步設計方法來解決併發問題。一般,這項工做涉及獲取一個後臺線程,在該線程上啓動所需的任務,而後在任務完成時向調用方發送通知(一般經過一個回調函數)。github
iOS 提供了一些技術,容許您異步執行任何任務,而無需本身管理線程。異步啓動任務的技術之一是 Grand Central Dispatch (GCD)。這種技術採用線程管理代碼,並將該代碼移動到系統級別。您所要作的就是定義要執行的任務,並將它們添加到適當的分派隊列中。GCD 負責建立所需的線程,並安排任務在這些線程上運行。因爲線程管理如今是系統的一部分,GCD 提供了任務管理和執行的總體方法,比傳統線程提供了更高的效率。編程
OperationQueue(操做隊列,api 類名爲 NSOperationQueue )是 Objective-C 對象,是對 GCD 的封裝。其做用很是相似於分派隊列。您定義要執行的任務,而後將它們添加到 OperationQueue 中, OperationQueue 處理這些任務的調度和執行。與 GCD 同樣, OperationQueue 爲您處理全部線程管理,確保在系統上儘量快速有效地執行任務。swift
接下來,就對如今工做中經常使用的這兩種技術進行比較和實例解析。api
GCD:安全
OperationQueue:網絡
OC 框架,更加面向對象,是對 GCD 的封裝。數據結構
iOS 2.0 推出的,蘋果推出 GCD 以後,對 NSOperation 的底層進行了所有重寫。
能夠設置隊列中每個操做的 QOS() 隊列的總體 QOS
操做相關
Operation做爲一個對象,爲咱們提供了更多的選擇:
任務依賴(addDependency),能夠跨隊列設置操做的依賴關係;
在隊列中的優先級(queuePriority)
服務質量(qualityOfService, iOS8+);
完成回調(void (^completionBlock)(void)
隊列相關
服務質量(qualityOfService, iOS8+);
最大併發操做數(maxConcurrentOperationCount),GCD 不易實現;
暫停/繼續(suspended);
取消全部操做(cancelAllOperations);
KVO 監聽隊列任務執行進度(progress, iOS13+);
接下來經過文字,結合實踐代碼(工程連接在文末)和運行效果 gif 圖對部分功能進行分析。
串行隊列中的任務按順序執行;可是不一樣串行隊列間沒有任何約束; 多個串行隊列同時執行時,不一樣隊列中任務執行是併發的效果。好比:火車站買票能夠有多個賣票口,可是每一個排的隊都是串行隊列,總體併發,單線串行。
注意防坑:串行隊列建立的位置。好比下面代碼示例中:在for循環內部建立時,每一個循環都是建立一個新的串行隊列,裏面只裝一個任務,多個串行隊列,結果總體上是併發的效果。想要串行效果,必須在for循環外部建立串行隊列。
串行隊列適合管理共享資源。保證了順序訪問,杜絕了資源競爭。
代碼示例:
private func serialExcuteByGCD(){ let lArr : [UIImageView] = [imageView1, imageView2, imageView3, imageView4] //串行隊列,異步執行時,只開一個子線程 let serialQ = DispatchQueue.init(label: "com.companyName.serial.downImage") for i in 0..<lArr.count{ let lImgV = lArr[i] //清空舊圖片 lImgV.image = nil //注意,防坑:串行隊列建立的位置,在這建立時,每一個循環都是一個新的串行隊列,裏面只裝一個任務,多個串行隊列,總體上是並行的效果。 // let serialQ = DispatchQueue.init(label: "com.companyName.serial.downImage") serialQ.async { print("第\(i)個 開始,%@",Thread.current) Downloader.downloadImageWithURLStr(urlStr: imageURLs[i]) { (img) in let lImgV = lArr[i] print("第\(i)個 結束") DispatchQueue.main.async { print("第\(i)個 切到主線程更新圖片") lImgV.image = img } if nil == img{ print("第\(i+1)個img is nil") } } } } }
gif 效果圖:
圖中下載時可順利拖動滾動條,是爲了說明下載在子線程,不影響UI交互
log:
第0個 開始 第0個 結束 第1個 開始 第0個 更新圖片 第1個 結束 第2個 開始 第1個 更新圖片 第2個 結束 第3個 開始 第2個 更新圖片 第3個 結束 第3個 更新圖片
由 log 可知: GCD 切到主線程也須要時間,切換完成以前,指令可能已經執行到下個循環了。可是看起來圖片仍是依次下載完成和顯示的,由於每一張圖切到主線程顯示都須要時間。
併發隊列依舊保證中任務按加入的前後順序開始(FIFO),可是沒法知道執行順序,執行時長和某一時刻的任務數。按 FIFO 開始後,他們之間不會相互等待。
好比:提交了 #1,#2,#3 任務到併發隊列,開始的順序是 #1,#2,#3。#2 和 #3 雖然開始的比 #1 晚,可是可能比 #1 執行結束的還要早。任務的執行是由系統決定的,因此執行時長和結束時間都沒法肯定。
須要用到併發隊列時,強烈建議 使用系統自帶的四種全局隊列之一。可是,當你須要使用 barrier 對隊列中任務進行柵欄時,只能使用自定義併發隊列。
Use a barrier to synchronize the execution of one or more tasks in your dispatch queue. When you add a barrier to a concurrent dispatch queue, the queue delays the execution of the barrier block (and any tasks submitted after the barrier) until all previously submitted tasks finish executing. After the previous tasks finish executing, the queue executes the barrier block by itself. Once the barrier block finishes, the queue resumes its normal execution behavior.
對比:barrier 和鎖的區別
代碼示例:
private func concurrentExcuteByGCD(){ let lArr : [UIImageView] = [imageView1, imageView2, imageView3, imageView4] for i in 0..<lArr.count{ let lImgV = lArr[i] //清空舊圖片 lImgV.image = nil //並行隊列:圖片下載任務按順序開始,可是是並行執行,不會相互等待,任務結束和圖片顯示順序是無序的,多個子線程同時執行,性能更佳。 let lConQ = DispatchQueue.init(label: "cusQueue", qos: .background, attributes: .concurrent) lConQ.async { print("第\(i)個開始,%@", Thread.current) Downloader.downloadImageWithURLStr(urlStr: imageURLs[i]) { (img) in let lImgV = lArr[i] print("第\(i)個結束") DispatchQueue.main.async { lImgV.image = img } if nil == img{ print("第\(i+1)個img is nil") } } } } }
gif 效果圖:
log:
第0個開始,%@ <NSThread: 0x600002de2e00>{number = 4, name = (null)} 第1個開始,%@ <NSThread: 0x600002dc65c0>{number = 6, name = (null)} 第2個開始,%@ <NSThread: 0x600002ddc8c0>{number = 8, name = (null)} 第3個開始,%@ <NSThread: 0x600002d0c8c0>{number = 7, name = (null)} 第0個結束 第3個結束 第1個結束 第2個結束
/** Submits a block for asynchronous execution on a main queue and returns immediately. */ static inline void dispatch_async_on_main_queue(void (^block)()) { if (NSThread.isMainThread) { block(); } else { dispatch_async(dispatch_get_main_queue(), block); } }
主隊列是串行隊列,每一個時間點只能有一個任務執行,所以若是耗時操做放到主隊列,會致使界面卡頓。
系統提供一個串行主隊列,4個 不一樣優先級的全局隊列。
用 dispatch_get_global_queue 方法獲取全局隊列時,第一個參數有 4 種類型可選:
DISPATCH_QUEUE_PRIORITY_HIGH
DISPATCH_QUEUE_PRIORITY_DEFAULT
DISPATCH_QUEUE_PRIORITY_LOW
DISPATCH_QUEUE_PRIORITY_BACKGROUND
串行隊列異步執行時,切到主線程刷 UI 也須要時間,切換完成以前,指令可能已經執行到下個循環了。可是看起來圖片仍是依次下載完成和顯示的,由於每一張圖切到主線程顯示都須要時間。詳見 demo 示例。
iOS8 以後,若是須要添加可被取消的任務,可使用 DispatchWorkItem 類,此類有 cancel 方法。
應該避免建立大量的串行隊列,若是但願併發執行大量任務,請將它們提交給全局併發隊列之一。建立串行隊列時,請嘗試爲每一個隊列肯定一個用途,例如保護資源或同步應用程序的某些關鍵行爲(如藍牙檢測結果須要有序處理的邏輯)。
調度隊列複製添加到它們中的塊,並在執行完成時釋放塊。
雖然隊列在執行小任務時比原始線程更有效,可是建立塊並在隊列上執行它們仍然存在開銷。若是一個塊執行的工做量太少,那麼內聯執行它可能比將它分派到隊列中要便宜得多。判斷一個塊是否工做量太少的方法是使用性能工具爲每一個路徑收集度量數據並進行比較。
您可能但願將 block 的部分代碼包含在 @autoreleasepool 中,以處理這些對象的內存管理。儘管 GCD 調度隊列擁有本身的自動釋放池,但它們不能保證這些池什麼時候耗盡。若是您的應用程序是內存受限的,那麼建立您本身的自動釋放池可讓您以更有規律的間隔釋放自動釋放對象的內存。
dispatch_after 函數並非在指定時間以後纔開始執行處理,而是在指定時間以後將任務追加到隊列中。這個時間並非絕對準確的。
代碼示例:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"2s後執行"); });
在多線程訪問可變變量時,是非線程安全的。可能致使程序崩潰。此時,能夠經過使用信號量(semaphore)技術,保證多線程處理某段代碼時,後面線程等待前面線程執行,保證了多線程的安全性。使用方法記兩個就好了,一個是wait(dispatch_semaphore_wait),一個是signal(dispatch_semaphore_signal)。
具體請參考文章Semaphore回顧
當每次迭代中執行工做與其餘全部迭代中執行的工做不一樣,且每一個循環完成的順序不重要時,能夠用 dispatch_apply 函數替換循環。注意:替換後, dispatch_apply 函數總體上是同步執行,內部 block 的執行類型(串行/併發)由隊列類型決定,可是串行隊列易死鎖,建議用併發隊列。
原循環:
for (i = 0; i < count; i++) { printf("%u\n",i); } printf("done");
優化後:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //count 是迭代的總次數。 dispatch_apply(count, queue, ^(size_t i) { printf("%u\n",i); }); //一樣在上面循環結束後才調用。 printf("done");
您應該確保您的任務代碼在每次迭代中完成合理數量的工做。與您分派到隊列的任何塊或函數同樣,調度該代碼以便執行會帶來開銷。若是循環的每次迭代只執行少許的工做,那麼調度代碼的開銷可能會超過將代碼分派到隊列可能帶來的性能優點。若是您在測試期間發現這一點是正確的,那麼您可使用步進來增長每一個循環迭代期間執行的工做量。經過大步前進,您能夠將原始循環的多個迭代集中到一個塊中,並按比例減小迭代次數。例如,若是您最初執行了 100次 迭代,但決定使用步長爲 4 的迭代,那麼您如今從每一個塊執行 4 次循環迭代,迭代次數爲 25次 。
一個隊列的不一樣任務能夠在多個線程執行嗎?
答:串行隊列,異步執行時,只開一個子線程;無所謂多個線程執行;
併發隊列,異步執行時,會自動開多個線程,能夠在多個線程併發執行不一樣的任務。
一個線程能夠同時執行多個隊列的任務嗎?
答:一個線程某個時間點只能執行一個任務,執行完畢後,可能執行到來自其餘隊列的任務(若是有的話)。好比:主線程除了執行主隊列中任務外,也可能會執行非主隊列中的任務。
隊列與線程關係示例圖:
qualityOfService 和 queuePriority 的區別是什麼?
答:
qualityOfService:
用於表示 operation 在獲取系統資源時的優先級,默認值:NSQualityOfServiceBackground,咱們能夠根據須要給 operation 賦不一樣的優化級,如最高優化級:NSQualityOfServiceUserInteractive。
queuePriority:
用於設置 operation 在 operationQueue 中的相對優化級,同一 queue 中優化級高的 operation(isReady 爲 YES) 會被優先執行。
須要注意區分 qualityOfService (在系統層面,operation 與其餘線程獲取資源的優先級) 與 queuePriority (同一 queue 中 operation 間執行的優化級)的區別。同時,須要注意 dependencies (嚴格控制執行順序)與 queuePriority (queue 內部相對優先級)的區別。
添加依賴後,隊列中網絡請求任務有依賴關係時,任務結束斷定以數據返回爲準仍是以發起請求爲準?
答:以發起請求爲準。分析過程詳見NSOperationQueue隊列中操做依賴相關思考
NSOperation
NSOperation 是一個"抽象類",不能直接使用。抽象類的用處是定義子類共有的屬性和方法。NSOperation 是基於 GCD 作的面向對象的封裝。相比較 GCD 使用更加簡單,而且提供了一些用 GCD 不是很好實現的功能。是蘋果公司推薦使用的併發技術。它有兩個子類:
NSOperationQueue
OperationQueue也是對 GCD 的高級封裝,更加面向對象,能夠實現 GCD 不方便實現的一些效果。被添加到隊列的操做默認是異步執行的。
PS:常見的抽象類有:
經過對不一樣操做設置依賴,或優先級,可實現 非FIFO 效果。
代碼示例:
func testDepedence(){ let op0 = BlockOperation.init { print("op0") } let op1 = BlockOperation.init { print("op1") } let op2 = BlockOperation.init { print("op2") } let op3 = BlockOperation.init { print("op3") } let op4 = BlockOperation.init { print("op4") } op0.addDependency(op1) op1.addDependency(op2) op0.queuePriority = .veryHigh op1.queuePriority = .normal op2.queuePriority = .veryLow op3.queuePriority = .low op4.queuePriority = .veryHigh gOpeQueue.addOperations([op0, op1, op2, op3, op4], waitUntilFinished: false) }
log:
op4 op2 op3 op1 op0
或
op4 op3 op2 op1 op0
說明:操做間不存在依賴時,按優先級執行;存在依賴時,按依賴關係前後執行(與無依賴關係的其餘任務相比,依賴集合的執行順序不肯定)
經過對隊列的isSuspended
屬性賦值,可實現隊列中未執行任務的暫停和繼續效果。正在執行的任務不受影響。
///暫停隊列,只對未執行中的任務有效。本例中對串行隊列的效果明顯。併發隊列因4個任務一開始就很容易一塊兒開始執行,即便掛起也沒法影響已處於執行狀態的任務。 @IBAction func pauseQueueItemDC(_ sender: Any) { gOpeQueue.isSuspended = true } ///恢復隊列,以前未開始執行的任務會開始執行 @IBAction func resumeQueueItemDC(_ sender: Any) { gOpeQueue.isSuspended = false }
gif 效果圖:
取消(cancel)時,有 3 種狀況:
1.操做在隊列中等待執行,這種狀況下,操做將不會被執行。
2.操做已經在執行中,此時,系統不會強制中止這個操做,可是,其 cancelled
屬性會被置爲 true 。
3.操做已完成,此時,cancel 無任何影響。
方法: cancelAllOperations。一樣只會對未執行的任務有效。
demo 中代碼:
deinit { gOpeQueue.cancelAllOperations() print("die:%@",self) }
經過設置操做間依賴,能夠實現 非FIFO 的指定順序效果。那麼,經過設置最大併發數爲 1 ,能夠實現指定順序效果嗎?
A:不能夠!
設置最大併發數爲 1 後,雖然每一個時間點只執行一個操做,可是操做的執行順序仍然基於其餘因素,如操做的依賴關係,操做的優先級(依賴關係比優先級級別更高,即先根據依賴關係排序;不存在依賴關係時,才根據優先級排序)。所以,序列化 操做隊列 不會提供與 GCD 中的序列 分派隊列 徹底相同的行爲。若是操做對象的執行順序對您很重要,那麼您應該在將操做添加到隊列以前使用 依賴關係 創建該順序,或改用 GCD 的 串行隊列 實現序列化效果。
Operation Queue的 block 中爲什麼無需使用 [weak self] 或 [unowned self] ?
A:即便隊列對象是爲全局的,self -> queue -> operation block -> self,的確會形成循環引用。可是在隊列裏的操做執行完畢時,隊列會自動釋放操做,自動解除循環引用。因此沒必要使用 [weak self] 或 [unowned self] 。
此外,這種循環引用在某些狀況下很是有用,你無需額外持有任何對象就可讓操做自動完成它的任務。好比下載頁面下載過程當中,退出有循環引用的界面時,若是不執行 cancelAllOperation 方法,能夠實現繼續執行剩餘隊列中下載任務的效果。
func addOperation(_ op: Operation)
Discussion:
Once added, the specified operation remains in the queue until it finishes executing.
Declaration
func addOperation(_ block: @escaping () -> Void)
Parameters
block
The block to execute from the operation. The block takes no parameters and has no return value.
Discussion
This method adds a single block to the receiver by first wrapping it in an operation object. You should not attempt to get a reference to the newly created operation object or determine its type information.
This property specifies the service level applied to operation objects added to the queue. If the operation object has an explicit service level set, that value is used instead.
資源競爭可能致使數據異常,死鎖,甚至因訪問野指針而崩潰。
func testDeadLock(){ //主隊列同步執行,會致使死鎖。block須要等待testDeadLock執行,而主隊列同步調用,又使其餘任務必須等待此block執行。因而造成了相互等待,就死鎖了。 DispatchQueue.main.sync { print("main block") } print("2") }
可是下面代碼不會死鎖,故串行隊列同步執行任務不必定死鎖。
- (void)testSynSerialQueue{ dispatch_queue_t myCustomQueue; myCustomQueue = dispatch_queue_create("com.example.MyCustomQueue", NULL); dispatch_async(myCustomQueue, ^{ printf("Do some work here.\n"); }); printf("The first block may or may not have run.\n"); dispatch_sync(myCustomQueue, ^{ printf("Do some more work here.\n"); }); printf("Both blocks have completed.\n"); }
代碼設計優先級:系統方法 > 並行 > 串行 > 鎖,簡記爲:西餅傳說
Concurrency Programming Guide
iOS Concurrency: Getting Started with NSOperation and Dispatch Queues
文中提到的知識點,「與其用操做對象淹沒隊列,不如批量建立這些對象。當一個批處理完成執行時,使用完成塊告訴應用程序建立一個新的批處理」,在最近的工做中的確有須要相似的需求,等有時間會進行總結,就做爲下一篇文章的預告吧。
本文由博客羣發一文多發等運營工具平臺 OpenWrite 發佈