iOS多線程之超實(shí)用理論+demo演示(可下載)

[toc]

背景簡(jiǎn)介

? ? ?在初學(xué)iOS相關(guān)知識(shí)過(guò)程中,大多都對(duì)多線程有些恐懼的心里,同時(shí)感覺(jué)工作中用上的概率不大。但是如果平時(shí)不多積累并學(xué)透多線程,當(dāng)工作中真的需要用到的時(shí)候,就很可能簡(jiǎn)單百度后把一些知識(shí)點(diǎn)稀里糊涂地就用到工作中了,殊不知里面有很多的坑,也有很多技巧需要在理論上先做了解,再結(jié)合實(shí)戰(zhàn),進(jìn)一步去體會(huì)多線程的魅力和強(qiáng)大。

? ? ?接下來(lái),就對(duì)多線程來(lái)源的背景進(jìn)行簡(jiǎn)單的介紹:

? ? ?在計(jì)算的早期,計(jì)算機(jī)可以執(zhí)行的最大工作量是由 CPU 的時(shí)鐘速度決定的。但是隨著技術(shù)的進(jìn)步和處理器設(shè)計(jì)的緊湊化,熱量和其他物理約束開(kāi)始限制處理器的最大時(shí)鐘速度。因此,芯片制造商尋找其他方法來(lái)提高芯片的總體性能。他們決定的解決方案是增加每個(gè)芯片上的處理器核心數(shù)量。通過(guò)增加內(nèi)核的數(shù)量,一個(gè)單獨(dú)的芯片可以每秒執(zhí)行更多的指令,而不用增加 CPU 的速度或改變芯片的大小或熱特性。唯一的問(wèn)題是如何利用額外的內(nèi)核。

? ? ?應(yīng)用程序使用多核的傳統(tǒng)方法是創(chuàng)建多個(gè)線程。與依賴(lài)線程不同,iOS 采用異步設(shè)計(jì)方法來(lái)解決并發(fā)問(wèn)題。通常,這項(xiàng)工作涉及獲取一個(gè)后臺(tái)線程,在該線程上啟動(dòng)所需的任務(wù),然后在任務(wù)完成時(shí)向調(diào)用方發(fā)送通知(通常通過(guò)一個(gè)回調(diào)函數(shù))。

? ? ?iOS 提供了一些技術(shù),允許您異步執(zhí)行任何任務(wù),而無(wú)需自己管理線程。異步啟動(dòng)任務(wù)的技術(shù)之一是 Grand Central Dispatch (GCD)。這種技術(shù)采用線程管理代碼,并將該代碼移動(dòng)到系統(tǒng)級(jí)別。您所要做的就是定義要執(zhí)行的任務(wù),并將它們添加到適當(dāng)?shù)姆峙申?duì)列中。GCD 負(fù)責(zé)創(chuàng)建所需的線程,并安排任務(wù)在這些線程上運(yùn)行。由于線程管理現(xiàn)在是系統(tǒng)的一部分,GCD 提供了任務(wù)管理和執(zhí)行的整體方法,比傳統(tǒng)線程提供了更高的效率。

? ? ?OperationQueue(操作隊(duì)列,api 類(lèi)名為 NSOperationQueue )是 Objective-C 對(duì)象,是對(duì) GCD 的封裝。其作用非常類(lèi)似于分派隊(duì)列。您定義要執(zhí)行的任務(wù),然后將它們添加到 OperationQueue 中, OperationQueue 處理這些任務(wù)的調(diào)度和執(zhí)行。與 GCD 一樣, OperationQueue 為您處理所有線程管理,確保在系統(tǒng)上盡可能快速有效地執(zhí)行任務(wù)。

? ? ?接下來(lái),就對(duì)現(xiàn)在工作中常用的這兩種技術(shù)進(jìn)行比較和實(shí)例解析。

GCD、OperationQueue 對(duì)比

核心理念

  • GCD的核心概念:將 任務(wù)(block) 添加到隊(duì)列,并且指定執(zhí)行任務(wù)的函數(shù)。
  • NSOperation 的核心概念:把 操作(異步) 添加到 隊(duì)列。

區(qū)別

  • GCD:

    • 將任務(wù)(block)添加到隊(duì)列(串行/并發(fā)/主隊(duì)列),并且指定任務(wù)執(zhí)行的函數(shù)(同步/異步)
    • GCD是底層的C語(yǔ)言構(gòu)成的API
    • iOS 4.0 推出的,針對(duì)多核處理器的并發(fā)技術(shù)
    • 在隊(duì)列中執(zhí)行的是由 block 構(gòu)成的任務(wù),這是一個(gè)輕量級(jí)的數(shù)據(jù)結(jié)構(gòu)
    • 要停止已經(jīng)加入 queue 的 block 需要寫(xiě)復(fù)雜的代碼
    • 需要通過(guò) Barrier(dispatch_barrier_async)或者同步任務(wù)設(shè)置任務(wù)之間的依賴(lài)關(guān)系
    • 只能設(shè)置隊(duì)列的優(yōu)先級(jí)
    • 高級(jí)功能:
      dispatch_once_t(一次性執(zhí)行, 多線程安全);
      dispatch_after(延遲);
      dispatch_group(調(diào)度組);
      dispatch_semaphore(信號(hào)量);
      dispatch_apply(優(yōu)化順序不敏感大體量for循環(huán));
  • OperationQueue:

    • OC 框架,更加面向?qū)ο?,是?duì) GCD 的封裝。

    • iOS 2.0 推出的,蘋(píng)果推出 GCD 之后,對(duì) NSOperation 的底層進(jìn)行了全部重寫(xiě)。

    • 可以設(shè)置隊(duì)列中每一個(gè)操作的 QOS() 隊(duì)列的整體 QOS

    • 操作相關(guān)
      Operation作為一個(gè)對(duì)象,為我們提供了更多的選擇:
      任務(wù)依賴(lài)(addDependency),可以跨隊(duì)列設(shè)置操作的依賴(lài)關(guān)系;
      在隊(duì)列中的優(yōu)先級(jí)(queuePriority)
      服務(wù)質(zhì)量(qualityOfService, iOS8+);
      完成回調(diào)(void (^completionBlock)(void)

    • 隊(duì)列相關(guān)
      服務(wù)質(zhì)量(qualityOfService, iOS8+);
      最大并發(fā)操作數(shù)(maxConcurrentOperationCount),GCD 不易實(shí)現(xiàn);
      暫停/繼續(xù)(suspended);
      取消所有操作(cancelAllOperations);
      KVO 監(jiān)聽(tīng)隊(duì)列任務(wù)執(zhí)行進(jìn)度(progress, iOS13+);

? ? ?接下來(lái)通過(guò)文字,結(jié)合實(shí)踐代碼(工程鏈接在文末)和運(yùn)行效果 gif 圖對(duì)部分功能進(jìn)行分析。

GCD

隊(duì)列

串行隊(duì)列(Serial Queues)

? ? ?串行隊(duì)列中的任務(wù)按順序執(zhí)行;但是不同串行隊(duì)列間沒(méi)有任何約束; 多個(gè)串行隊(duì)列同時(shí)執(zhí)行時(shí),不同隊(duì)列中任務(wù)執(zhí)行是并發(fā)的效果。比如:火車(chē)站買(mǎi)票可以有多個(gè)賣(mài)票口,但是每個(gè)排的隊(duì)都是串行隊(duì)列,整體并發(fā),單線串行。

? ? ?注意防坑:串行隊(duì)列創(chuàng)建的位置。比如下面代碼示例中:在for循環(huán)內(nèi)部創(chuàng)建時(shí),每個(gè)循環(huán)都是創(chuàng)建一個(gè)新的串行隊(duì)列,里面只裝一個(gè)任務(wù),多個(gè)串行隊(duì)列,結(jié)果整體上是并發(fā)的效果。想要串行效果,必須在for循環(huán)外部創(chuàng)建串行隊(duì)列。

? ? ?串行隊(duì)列適合管理共享資源。保證了順序訪問(wèn),杜絕了資源競(jìng)爭(zhēng)。

? ? ? 代碼示例:

    private func serialExcuteByGCD(){
        let lArr : [UIImageView] = [imageView1, imageView2, imageView3, imageView4]
        
        //串行隊(duì)列,異步執(zhí)行時(shí),只開(kāi)一個(gè)子線程
        let serialQ = DispatchQueue.init(label: "com.companyName.serial.downImage")
        
        for i in 0..<lArr.count{
            let lImgV = lArr[i]
            
            //清空舊圖片
            lImgV.image = nil
            
         //注意,防坑:串行隊(duì)列創(chuàng)建的位置,在這創(chuàng)建時(shí),每個(gè)循環(huán)都是一個(gè)新的串行隊(duì)列,里面只裝一個(gè)任務(wù),多個(gè)串行隊(duì)列,整體上是并行的效果。
            //            let serialQ = DispatchQueue.init(label: "com.companyName.serial.downImage")
            
            serialQ.async {
                
                print("第\(i)個(gè) 開(kāi)始,%@",Thread.current)
                Downloader.downloadImageWithURLStr(urlStr: imageURLs[i]) { (img) in
                    let lImgV = lArr[i]
                    
                    print("第\(i)個(gè) 結(jié)束")
                    DispatchQueue.main.async {
                        print("第\(i)個(gè) 切到主線程更新圖片")
                        lImgV.image = img
                    }
                    if nil == img{
                        print("第\(i+1)個(gè)img is nil")
                    }
                }
            }
        }
    }

gif 效果圖:

serialGCD

圖中下載時(shí)可順利拖動(dòng)滾動(dòng)條,是為了說(shuō)明下載在子線程,不影響UI交互

log:

第0個(gè) 開(kāi)始
第0個(gè) 結(jié)束
第1個(gè) 開(kāi)始
第0個(gè) 更新圖片
第1個(gè) 結(jié)束
第2個(gè) 開(kāi)始
第1個(gè) 更新圖片
第2個(gè) 結(jié)束
第3個(gè) 開(kāi)始
第2個(gè) 更新圖片
第3個(gè) 結(jié)束
第3個(gè) 更新圖片

? ? ? 由 log 可知: GCD 切到主線程也需要時(shí)間,切換完成之前,指令可能已經(jīng)執(zhí)行到下個(gè)循環(huán)了。但是看起來(lái)圖片還是依次下載完成和顯示的,因?yàn)槊恳粡垐D切到主線程顯示都需要時(shí)間。

并發(fā)隊(duì)列(Concurrent Queues)

? ? ?并發(fā)隊(duì)列依舊保證中任務(wù)按加入的先后順序開(kāi)始(FIFO),但是無(wú)法知道執(zhí)行順序,執(zhí)行時(shí)長(zhǎng)和某一時(shí)刻的任務(wù)數(shù)。按 FIFO 開(kāi)始后,他們之間不會(huì)相互等待。

? ? ?比如:提交了 #1,#2,#3 任務(wù)到并發(fā)隊(duì)列,開(kāi)始的順序是 #1,#2,#3。#2 和 #3 雖然開(kāi)始的比 #1 晚,但是可能比 #1 執(zhí)行結(jié)束的還要早。任務(wù)的執(zhí)行是由系統(tǒng)決定的,所以執(zhí)行時(shí)長(zhǎng)和結(jié)束時(shí)間都無(wú)法確定。

? ? ?需要用到并發(fā)隊(duì)列時(shí),強(qiáng)烈建議 使用系統(tǒng)自帶的四種全局隊(duì)列之一。但是,當(dāng)你需要使用 barrier 對(duì)隊(duì)列中任務(wù)進(jìn)行柵欄時(shí),只能使用自定義并發(fā)隊(duì)列。

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.

? ? ?對(duì)比:barrier 和鎖的區(qū)別

  • 依賴(lài)對(duì)象不同,barrier 依賴(lài)的對(duì)象是自定義并發(fā)隊(duì)列,鎖操作依賴(lài)的對(duì)象是線程。
  • 作用不同,barrier 起到自定義并發(fā)隊(duì)列中柵欄的作用;鎖起到多線程操作時(shí)防止資源競(jìng)爭(zhēng)的作用。

? ? ? 代碼示例:

private func concurrentExcuteByGCD(){
        let lArr : [UIImageView] = [imageView1, imageView2, imageView3, imageView4]
        
        for i in 0..<lArr.count{
            let lImgV = lArr[i]
            
            //清空舊圖片
            lImgV.image = nil
            
            //并行隊(duì)列:圖片下載任務(wù)按順序開(kāi)始,但是是并行執(zhí)行,不會(huì)相互等待,任務(wù)結(jié)束和圖片顯示順序是無(wú)序的,多個(gè)子線程同時(shí)執(zhí)行,性能更佳。
            let lConQ = DispatchQueue.init(label: "cusQueue", qos: .background, attributes: .concurrent)
            lConQ.async {
                print("第\(i)個(gè)開(kāi)始,%@", Thread.current)
                Downloader.downloadImageWithURLStr(urlStr: imageURLs[i]) { (img) in
                    let lImgV = lArr[i]
                      print("第\(i)個(gè)結(jié)束")
                    DispatchQueue.main.async {
                        lImgV.image = img
                    }
                    if nil == img{
                        print("第\(i+1)個(gè)img is nil")
                    }
                }
            }
        }
    }

gif 效果圖:


conGCD

log:

第0個(gè)開(kāi)始,%@ <NSThread: 0x600002de2e00>{number = 4, name = (null)}
第1個(gè)開(kāi)始,%@ <NSThread: 0x600002dc65c0>{number = 6, name = (null)}
第2個(gè)開(kāi)始,%@ <NSThread: 0x600002ddc8c0>{number = 8, name = (null)}
第3個(gè)開(kāi)始,%@ <NSThread: 0x600002d0c8c0>{number = 7, name = (null)}
第0個(gè)結(jié)束
第3個(gè)結(jié)束
第1個(gè)結(jié)束
第2個(gè)結(jié)束

串行、并發(fā)隊(duì)列對(duì)比圖

gcd-cheatsheet

注意事項(xiàng)

  • 無(wú)論串行還是并發(fā)隊(duì)列,都是 FIFO ;
    一般創(chuàng)建 任務(wù)(blocks)和加任務(wù)到隊(duì)列是在主線程,但是任務(wù)執(zhí)行一般是在其他線程(asyc)。需要刷新 UI 時(shí),如果當(dāng)前不再主線程,需要切回主線程執(zhí)行。當(dāng)不確定當(dāng)前線程是否在主線程時(shí),可以使用下面代碼:
/**
 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);
    }
}
  • 主隊(duì)列是串行隊(duì)列,每個(gè)時(shí)間點(diǎn)只能有一個(gè)任務(wù)執(zhí)行,因此如果耗時(shí)操作放到主隊(duì)列,會(huì)導(dǎo)致界面卡頓。

  • 系統(tǒng)提供一個(gè)串行主隊(duì)列,<u>4個(gè)</u> 不同優(yōu)先級(jí)的全局隊(duì)列。
    用 dispatch_get_global_queue 方法獲取全局隊(duì)列時(shí),第一個(gè)參數(shù)有 4 種類(lèi)型可選:

    • DISPATCH_QUEUE_PRIORITY_HIGH
      
    • DISPATCH_QUEUE_PRIORITY_DEFAULT
      
    • DISPATCH_QUEUE_PRIORITY_LOW
      
    • DISPATCH_QUEUE_PRIORITY_BACKGROUND
      
  • 串行隊(duì)列異步執(zhí)行時(shí),切到主線程刷 UI 也需要時(shí)間,切換完成之前,指令可能已經(jīng)執(zhí)行到下個(gè)循環(huán)了。但是看起來(lái)圖片還是依次下載完成和顯示的,因?yàn)槊恳粡垐D切到主線程顯示都需要時(shí)間。詳見(jiàn) demo 示例。

  • iOS8 之后,如果需要添加可被取消的任務(wù),可以使用 DispatchWorkItem 類(lèi),此類(lèi)有 cancel 方法。

  • 應(yīng)該避免創(chuàng)建大量的串行隊(duì)列,如果希望并發(fā)執(zhí)行大量任務(wù),請(qǐng)將它們提交給全局并發(fā)隊(duì)列之一。創(chuàng)建串行隊(duì)列時(shí),請(qǐng)嘗試為每個(gè)隊(duì)列確定一個(gè)用途,例如保護(hù)資源或同步應(yīng)用程序的某些關(guān)鍵行為(如藍(lán)牙檢測(cè)結(jié)果需要有序處理的邏輯)。

block(塊)相關(guān)

? ? ?調(diào)度隊(duì)列復(fù)制添加到它們中的塊,并在執(zhí)行完成時(shí)釋放塊。
? ? ?雖然隊(duì)列在執(zhí)行小任務(wù)時(shí)比原始線程更有效,但是創(chuàng)建塊并在隊(duì)列上執(zhí)行它們?nèi)匀淮嬖陂_(kāi)銷(xiāo)。如果一個(gè)塊執(zhí)行的工作量太少,那么內(nèi)聯(lián)執(zhí)行它可能比將它分派到隊(duì)列中要便宜得多。判斷一個(gè)塊是否工作量太少的方法是使用性能工具為每個(gè)路徑收集度量數(shù)據(jù)并進(jìn)行比較。
? ? ?您可能希望將 block 的部分代碼包含在 @autoreleasepool 中,以處理這些對(duì)象的內(nèi)存管理。盡管 GCD 調(diào)度隊(duì)列擁有自己的自動(dòng)釋放池,但它們不能保證這些池何時(shí)耗盡。如果您的應(yīng)用程序是內(nèi)存受限的,那么創(chuàng)建您自己的自動(dòng)釋放池可以讓您以更有規(guī)律的間隔釋放自動(dòng)釋放對(duì)象的內(nèi)存。

dispatch_after

? ? ?dispatch_after 函數(shù)并不是在指定時(shí)間之后才開(kāi)始執(zhí)行處理,而是在指定時(shí)間之后將任務(wù)追加到隊(duì)列中。這個(gè)時(shí)間并不是絕對(duì)準(zhǔn)確的。
? 代碼示例:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"2s后執(zhí)行");
    });

dispatch_semaphore

? ? ? 在多線程訪問(wèn)可變變量時(shí),是非線程安全的??赡軐?dǎo)致程序崩潰。此時(shí),可以通過(guò)使用信號(hào)量(semaphore)技術(shù),保證多線程處理某段代碼時(shí),后面線程等待前面線程執(zhí)行,保證了多線程的安全性。使用方法記兩個(gè)就行了,一個(gè)是wait(dispatch_semaphore_wait),一個(gè)是signal(dispatch_semaphore_signal)。

具體請(qǐng)參考文章Semaphore回顧

dispatch_apply

? ? ?當(dāng)每次迭代中執(zhí)行工作與其他所有迭代中執(zhí)行的工作不同,且每個(gè)循環(huán)完成的順序不重要時(shí),可以用 dispatch_apply 函數(shù)替換循環(huán)。注意:替換后, dispatch_apply 函數(shù)整體上是同步執(zhí)行,內(nèi)部 block 的執(zhí)行類(lèi)型(串行/并發(fā))由隊(duì)列類(lèi)型決定,但是串行隊(duì)列易死鎖,建議用并發(fā)隊(duì)列。

原循環(huán):

for (i = 0; i < count; i++) {
   printf("%u\n",i);
}
printf("done");

優(yōu)化后:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
 
 //count 是迭代的總次數(shù)。
dispatch_apply(count, queue, ^(size_t i) {
   printf("%u\n",i);
});

//同樣在上面循環(huán)結(jié)束后才調(diào)用。
printf("done");

? ? ?您應(yīng)該確保您的任務(wù)代碼在每次迭代中完成合理數(shù)量的工作。與您分派到隊(duì)列的任何塊或函數(shù)一樣,調(diào)度該代碼以便執(zhí)行會(huì)帶來(lái)開(kāi)銷(xiāo)。如果循環(huán)的每次迭代只執(zhí)行少量的工作,那么調(diào)度代碼的開(kāi)銷(xiāo)可能會(huì)超過(guò)將代碼分派到隊(duì)列可能帶來(lái)的性能優(yōu)勢(shì)。如果您在測(cè)試期間發(fā)現(xiàn)這一點(diǎn)是正確的,那么您可以使用步進(jìn)來(lái)增加每個(gè)循環(huán)迭代期間執(zhí)行的工作量。通過(guò)大步前進(jìn),您可以將原始循環(huán)的多個(gè)迭代集中到一個(gè)塊中,并按比例減少迭代次數(shù)。例如,如果您最初執(zhí)行了 100次 迭代,但決定使用步長(zhǎng)為 4 的迭代,那么您現(xiàn)在從每個(gè)塊執(zhí)行 4 次循環(huán)迭代,迭代次數(shù)為 25次 。

自問(wèn)自答

  • 一個(gè)隊(duì)列的不同任務(wù)可以在多個(gè)線程執(zhí)行嗎?
    答:串行隊(duì)列,異步執(zhí)行時(shí),只開(kāi)一個(gè)子線程;無(wú)所謂多個(gè)線程執(zhí)行;
    并發(fā)隊(duì)列,異步執(zhí)行時(shí),會(huì)自動(dòng)開(kāi)多個(gè)線程,可以在多個(gè)線程并發(fā)執(zhí)行不同的任務(wù)。

  • 一個(gè)線程可以同時(shí)執(zhí)行多個(gè)隊(duì)列的任務(wù)嗎?
    答:一個(gè)線程某個(gè)時(shí)間點(diǎn)只能執(zhí)行一個(gè)任務(wù),執(zhí)行完畢后,可能執(zhí)行到來(lái)自其他隊(duì)列的任務(wù)(如果有的話)。比如:主線程除了執(zhí)行主隊(duì)列中任務(wù)外,也可能會(huì)執(zhí)行非主隊(duì)列中的任務(wù)。

    隊(duì)列與線程關(guān)系示例圖:


    queues & threads
  • qualityOfService 和 queuePriority 的區(qū)別是什么?
    答:
    qualityOfService:
    ? ? ?用于表示 operation 在獲取系統(tǒng)資源時(shí)的優(yōu)先級(jí),默認(rèn)值:NSQualityOfServiceBackground,我們可以根據(jù)需要給 operation 賦不同的優(yōu)化級(jí),如最高優(yōu)化級(jí):NSQualityOfServiceUserInteractive。
    queuePriority:
    ? ? ?用于設(shè)置 operation 在 operationQueue 中的相對(duì)優(yōu)化級(jí),同一 queue 中優(yōu)化級(jí)高的 operation(isReady 為 YES) 會(huì)被優(yōu)先執(zhí)行。
    ? ? ?需要注意區(qū)分 qualityOfService (在系統(tǒng)層面,operation 與其他線程獲取資源的優(yōu)先級(jí)) 與 queuePriority (同一 queue 中 operation 間執(zhí)行的優(yōu)化級(jí))的區(qū)別。同時(shí),需要注意 dependencies (嚴(yán)格控制執(zhí)行順序)與 queuePriority (queue 內(nèi)部相對(duì)優(yōu)先級(jí))的區(qū)別。

  • 添加依賴(lài)后,隊(duì)列中網(wǎng)絡(luò)請(qǐng)求任務(wù)有依賴(lài)關(guān)系時(shí),任務(wù)結(jié)束判定以數(shù)據(jù)返回為準(zhǔn)還是以發(fā)起請(qǐng)求為準(zhǔn)?
    答:以發(fā)起請(qǐng)求為準(zhǔn)。分析過(guò)程詳見(jiàn)NSOperationQueue隊(duì)列中操作依賴(lài)相關(guān)思考

OperationQueue

  • NSOperation
    ? ? ?NSOperation 是一個(gè)"抽象類(lèi)",不能直接使用。抽象類(lèi)的用處是定義子類(lèi)共有的屬性和方法。NSOperation 是基于 GCD 做的面向?qū)ο蟮姆庋b。相比較 GCD 使用更加簡(jiǎn)單,并且提供了一些用 GCD 不是很好實(shí)現(xiàn)的功能。是蘋(píng)果公司推薦使用的并發(fā)技術(shù)。它有兩個(gè)子類(lèi):

    • NSInvocationOperation (調(diào)用操作)
    • NSBlockOperation (塊操作)
      ? ? ?一般常用NSBlockOperation,代碼簡(jiǎn)單,同時(shí)由于閉包性使它沒(méi)有傳參問(wèn)題。任務(wù)被封裝在 NSOperation 的子類(lèi)實(shí)例類(lèi)對(duì)象里,一個(gè) NSOperation 子類(lèi)對(duì)象可以添加多個(gè)任務(wù) block 和 一個(gè)執(zhí)行完成 block ,當(dāng)其關(guān)聯(lián)的所有 block 執(zhí)行完時(shí),就認(rèn)為操作結(jié)束了。
  • NSOperationQueue
    ? ? ? OperationQueue也是對(duì) GCD 的高級(jí)封裝,更加面向?qū)ο?,可以?shí)現(xiàn) GCD 不方便實(shí)現(xiàn)的一些效果。被添加到隊(duì)列的操作默認(rèn)是異步執(zhí)行的。

PS:常見(jiàn)的抽象類(lèi)有:

  • UIGestureRecognizer
  • CAAnimation
  • CAPropertyAnimation

可以實(shí)現(xiàn) 非FIFO 效果

通過(guò)對(duì)不同操作設(shè)置依賴(lài),或優(yōu)先級(jí),可實(shí)現(xiàn) 非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

說(shuō)明:操作間不存在依賴(lài)時(shí),按優(yōu)先級(jí)執(zhí)行;存在依賴(lài)時(shí),按依賴(lài)關(guān)系先后執(zhí)行(與無(wú)依賴(lài)關(guān)系的其他任務(wù)相比,依賴(lài)集合的執(zhí)行順序不確定)

隊(duì)列暫停/繼續(xù)

通過(guò)對(duì)隊(duì)列的isSuspended屬性賦值,可實(shí)現(xiàn)隊(duì)列中未執(zhí)行任務(wù)的暫停和繼續(xù)效果。正在執(zhí)行的任務(wù)不受影響。

///暫停隊(duì)列,只對(duì)未執(zhí)行中的任務(wù)有效。本例中對(duì)串行隊(duì)列的效果明顯。并發(fā)隊(duì)列因4個(gè)任務(wù)一開(kāi)始就很容易一起開(kāi)始執(zhí)行,即使掛起也無(wú)法影響已處于執(zhí)行狀態(tài)的任務(wù)。
    @IBAction func pauseQueueItemDC(_ sender: Any) {
        gOpeQueue.isSuspended = true
    }
    
    ///恢復(fù)隊(duì)列,之前未開(kāi)始執(zhí)行的任務(wù)會(huì)開(kāi)始執(zhí)行
    @IBAction func resumeQueueItemDC(_ sender: Any) {
       gOpeQueue.isSuspended = false
    }

gif 效果圖:


pauseResume

取消操作

  • 一旦添加到操作隊(duì)列中,操作對(duì)象實(shí)際上歸隊(duì)列所有,不能刪除。取消操作的唯一方法是取消它??梢酝ㄟ^(guò)調(diào)用單個(gè)操作對(duì)象的 cancel 方法來(lái)取消單個(gè)操作對(duì)象,也可以通過(guò)調(diào)用隊(duì)列對(duì)象的 cancelAllOperations 方法來(lái)取消隊(duì)列中的所有操作對(duì)象。
  • 更常見(jiàn)的做法是取消所有隊(duì)列操作,以響應(yīng)某些重要事件,如應(yīng)用程序退出或用戶(hù)專(zhuān)門(mén)請(qǐng)求取消,而不是有選擇地取消操作。

取消單個(gè)操作對(duì)象

取消(cancel)時(shí),有 3 種情況:
1.操作在隊(duì)列中等待執(zhí)行,這種情況下,操作將不會(huì)被執(zhí)行。
2.操作已經(jīng)在執(zhí)行中,此時(shí),系統(tǒng)不會(huì)強(qiáng)制停止這個(gè)操作,但是,其 cancelled屬性會(huì)被置為 true 。
3.操作已完成,此時(shí),cancel 無(wú)任何影響。

取消隊(duì)列中的所有操作對(duì)象

方法: cancelAllOperations。同樣只會(huì)對(duì)未執(zhí)行的任務(wù)有效。
demo 中代碼:

    deinit {
        gOpeQueue.cancelAllOperations()
        print("die:%@",self)
    }

自問(wèn)自答

  • 通過(guò)設(shè)置操作間依賴(lài),可以實(shí)現(xiàn) 非FIFO 的指定順序效果。那么,通過(guò)設(shè)置最大并發(fā)數(shù)為 1 ,可以實(shí)現(xiàn)指定順序效果嗎?
    A:不可以!
    設(shè)置最大并發(fā)數(shù)為 1 后,雖然每個(gè)時(shí)間點(diǎn)只執(zhí)行一個(gè)操作,但是操作的執(zhí)行順序仍然基于其他因素,如操作的依賴(lài)關(guān)系,操作的優(yōu)先級(jí)(依賴(lài)關(guān)系比優(yōu)先級(jí)級(jí)別更高,即先根據(jù)依賴(lài)關(guān)系排序;不存在依賴(lài)關(guān)系時(shí),才根據(jù)優(yōu)先級(jí)排序)。因此,序列化 操作隊(duì)列 不會(huì)提供與 GCD 中的序列 分派隊(duì)列 完全相同的行為。如果操作對(duì)象的執(zhí)行順序?qū)δ苤匾?,那么您?yīng)該在將操作添加到隊(duì)列之前使用 依賴(lài)關(guān)系 建立該順序,或改用 GCD 的 串行隊(duì)列 實(shí)現(xiàn)序列化效果。

  • Operation Queue的 block 中為何無(wú)需使用 [weak self] 或 [unowned self] ?
    A:即使隊(duì)列對(duì)象是為全局的,self -> queue -> operation block -> self,的確會(huì)造成循環(huán)引用。但是在隊(duì)列里的操作執(zhí)行完畢時(shí),隊(duì)列會(huì)自動(dòng)釋放操作,自動(dòng)解除循環(huán)引用。所以不必使用 [weak self] 或 [unowned self] 。
    此外,這種循環(huán)引用在某些情況下非常有用,你無(wú)需額外持有任何對(duì)象就可以讓操作自動(dòng)完成它的任務(wù)。比如下載頁(yè)面下載過(guò)程中,退出有循環(huán)引用的界面時(shí),如果不執(zhí)行 cancelAllOperation 方法,可以實(shí)現(xiàn)繼續(xù)執(zhí)行剩余隊(duì)列中下載任務(wù)的效果。

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.

  • 操作的 QOS 和隊(duì)列的 QOS 有何關(guān)系?
    A:隊(duì)列的 QOS 設(shè)置,會(huì)自動(dòng)把較低優(yōu)先級(jí)的操作提升到與隊(duì)列相同優(yōu)先級(jí)。(原更高優(yōu)先級(jí)操作的優(yōu)先級(jí)保持不變)。后續(xù)添加進(jìn)隊(duì)列的操作,優(yōu)先級(jí)低于隊(duì)列優(yōu)先級(jí)時(shí),也會(huì)被自動(dòng)提升到與隊(duì)列相同的優(yōu)先級(jí)。
    注意,蘋(píng)果文檔如下的解釋是錯(cuò)誤的 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.
    原因詳見(jiàn):Can NSOperation have a lower qualityOfService than NSOperationQueue?

常見(jiàn)問(wèn)題

如何解決資源競(jìng)爭(zhēng)問(wèn)題

資源競(jìng)爭(zhēng)可能導(dǎo)致數(shù)據(jù)異常,死鎖,甚至因訪問(wèn)野指針而崩潰。

  • 對(duì)于有明顯先后依賴(lài)關(guān)系的任務(wù),最佳方案是 GCD串行隊(duì)列,可以在不使用線程鎖時(shí)保證資源互斥。
  • 其他情況,對(duì)存在資源競(jìng)爭(zhēng)的代碼加鎖或使用信號(hào)量(初始參數(shù)填1,表示只允許一條線程訪問(wèn)資源)。
  • 串行隊(duì)列同步執(zhí)行時(shí),如果有任務(wù)相互等待,會(huì)死鎖。
    比如:在主線程上同步執(zhí)行任務(wù)時(shí),因任務(wù)和之前已加入主隊(duì)列但未執(zhí)行的任務(wù)會(huì)相互等待,導(dǎo)致死鎖。
  func testDeadLock(){
        //主隊(duì)列同步執(zhí)行,會(huì)導(dǎo)致死鎖。block需要等待testDeadLock執(zhí)行,而主隊(duì)列同步調(diào)用,又使其他任務(wù)必須等待此block執(zhí)行。于是形成了相互等待,就死鎖了。
        DispatchQueue.main.sync {
            print("main block")
        }
        print("2")
    }

但是下面代碼不會(huì)死鎖,故串行隊(duì)列同步執(zhí)行任務(wù)不一定死鎖。

- (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");
}

如何提高代碼效率

“西餅傳說(shuō)”

代碼設(shè)計(jì)優(yōu)先級(jí):系統(tǒng)方法 > 并行 > 串行 > 鎖,簡(jiǎn)記為:<u>西餅傳說(shuō)</u>

  • 盡可能依賴(lài) 系統(tǒng) 框架。實(shí)現(xiàn)并發(fā)性的最佳方法是利用系統(tǒng)框架提供的內(nèi)置并發(fā)性。
  • 盡早識(shí)別系列任務(wù),并盡可能使它們更加 并行。如果因?yàn)槟硞€(gè)任務(wù)依賴(lài)于某個(gè)共享資源而必須連續(xù)執(zhí)行該任務(wù),請(qǐng)考慮更改體系結(jié)構(gòu)以刪除該共享資源。您可以考慮為每個(gè)需要資源的客戶(hù)機(jī)制作資源的副本,或者完全消除該資源。
  • 不使用鎖來(lái)保護(hù)某些共享資源,而是指定一個(gè) 串行隊(duì)列 (或使用操作對(duì)象依賴(lài)項(xiàng))以正確的順序執(zhí)行任務(wù)。
  • 避免使用 。GCD 調(diào)度隊(duì)列操作隊(duì)列 提供的支持使得在大多數(shù)情況下不需要鎖定。

確定操作對(duì)象的適當(dāng)范圍

  • 盡管可以向操作隊(duì)列中添加任意大量的操作,但這樣做通常是不切實(shí)際的。與任何對(duì)象一樣,NSOperation 類(lèi)的實(shí)例消耗內(nèi)存,并且具有與其執(zhí)行相關(guān)的實(shí)際成本。如果您的每個(gè)操作對(duì)象只執(zhí)行少量的工作,并且您創(chuàng)建了數(shù)以萬(wàn)計(jì)的操作對(duì)象,那么您可能會(huì)發(fā)現(xiàn),您花在調(diào)度操作上的時(shí)間比花在實(shí)際工作上的時(shí)間更多。如果您的應(yīng)用程序已經(jīng)受到內(nèi)存限制,那么您可能會(huì)發(fā)現(xiàn),僅僅在內(nèi)存中擁有數(shù)萬(wàn)個(gè)操作對(duì)象就可能進(jìn)一步降低性能。
  • 有效使用操作的關(guān)鍵是 <u> 在你需要做的工作量和保持計(jì)算機(jī)忙碌之間找到一個(gè)適當(dāng)?shù)钠胶?lt;/u> 。盡量確保你的業(yè)務(wù)做了合理的工作量。例如,如果您的應(yīng)用程序創(chuàng)建了 100 個(gè)操作對(duì)象來(lái)對(duì) 100 個(gè)不同的值執(zhí)行相同的任務(wù),那么可以考慮創(chuàng)建 10 個(gè)操作對(duì)象來(lái)處理每個(gè)值。
  • 您還應(yīng)該避免將大量操作一次性添加到隊(duì)列中,或者避免連續(xù)地將操作對(duì)象添加到隊(duì)列中的速度快于處理它們的速度。與其用操作對(duì)象淹沒(méi)隊(duì)列,不如<u>批量創(chuàng)建這些對(duì)象。當(dāng)一個(gè)批處理完成執(zhí)行時(shí),使用完成塊告訴應(yīng)用程序創(chuàng)建一個(gè)新的批處理</u>。當(dāng)您有很多工作要做時(shí),您希望保持隊(duì)列中充滿(mǎn)足夠的操作,以便計(jì)算機(jī)保持忙碌,但是您<u>不希望一次創(chuàng)建太多操作,以至于應(yīng)用程序耗盡內(nèi)存。</u>
  • 當(dāng)然,您創(chuàng)建的操作對(duì)象的數(shù)量以及在每個(gè)操作對(duì)象中執(zhí)行的工作量是可變的,并且完全取決于您的應(yīng)用程序。你應(yīng)該經(jīng)常使用像 Instruments 這樣的工具來(lái)幫助你在效率和速度之間找到一個(gè)適當(dāng)?shù)钠胶狻S嘘P(guān) Instruments 和其他可用于為代碼收集度量標(biāo)準(zhǔn)的性能工具的概述,請(qǐng)參閱 性能概述

術(shù)語(yǔ)解釋摘錄

  • 異步任務(wù)(asynchronous tasks):由一個(gè)線程啟動(dòng),但實(shí)際上在另一個(gè)線程上運(yùn)行,利用額外的處理器資源更快地完成工作。
  • 互斥(mutex):提供對(duì)共享資源的互斥訪問(wèn)的鎖。
    互斥鎖一次只能由一個(gè)線程持有。試圖獲取由不同線程持有的互斥對(duì)象會(huì)使當(dāng)前線程處于休眠狀態(tài),直到最終獲得鎖為止。
  • 進(jìn)程(process):應(yīng)用軟件或程序的運(yùn)行時(shí)實(shí)例。
    進(jìn)程有自己的虛擬內(nèi)存空間和系統(tǒng)資源(包括端口權(quán)限) ,這些資源獨(dú)立于分配給其他程序的資源。一個(gè)進(jìn)程總是包含至少一個(gè)線程(主線程) ,并且可能包含任意數(shù)量的其他線程。
  • 信號(hào)量(semaphore):限制對(duì)共享資源訪問(wèn)的受保護(hù)變量。
    互斥(Mutexes)和條件(conditions)都是不同類(lèi)型的信號(hào)量。
  • 任務(wù)(task),表示需要執(zhí)行的工作量。
  • 線程(thread):進(jìn)程中的執(zhí)行流程。
    每個(gè)線程都有自己的堆??臻g,但在其他方面與同一進(jìn)程中的其他線程共享內(nèi)存。
  • 運(yùn)行循環(huán)(run loop): 一個(gè)事件處理循環(huán),
    接收事件并派發(fā)到適當(dāng)?shù)奶幚沓绦颉?/li>

官方并發(fā)編程詞匯表

本文 demo 地址

MultiThreadDemo

參考文章

Concurrency Programming Guide
iOS Concurrency: Getting Started with NSOperation and Dispatch Queues

下節(jié)預(yù)告

文中提到的知識(shí)點(diǎn),<u>“與其用操作對(duì)象淹沒(méi)隊(duì)列,不如批量創(chuàng)建這些對(duì)象。當(dāng)一個(gè)批處理完成執(zhí)行時(shí),使用完成塊告訴應(yīng)用程序創(chuàng)建一個(gè)新的批處理”</u>,在最近的工作中的確有需要類(lèi)似的需求,等有時(shí)間會(huì)進(jìn)行總結(jié),就作為下一篇文章的預(yù)告吧。

本文由博客群發(fā)一文多發(fā)等運(yùn)營(yíng)工具平臺(tái) OpenWrite 發(fā)布

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容