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

背景簡介

     在初學iOS相關知識過程中,大多都對多線程有些恐懼的心裏,同時感覺工作中用上的概率不大。但是如果平時不多積累並學透多線程,當工作中真的需要用到的時候,就很可能簡單百度後把一些知識點稀里糊塗地就用到工作中了,殊不知裏面有很多的坑,也有很多技巧需要在理論上先做了解,再結合實戰,進一步去體會多線程的魅力和強大。

     接下來,就對多線程來源的背景進行簡單的介紹:

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

     應用程序使用多核的傳統方法是創建多個線程。與依賴線程不同,iOS 採用異步設計方法來解決並發問題。通常,這項工作涉及獲取一個後台線程,在該線程上啟動所需的任務,然後在任務完成時向調用方發送通知(通常通過一個回調函數)。

     iOS 提供了一些技術,允許您異步執行任何任務,而無需自己管理線程。異步啟動任務的技術之一是 Grand Central Dispatch (GCD)。這種技術採用線程管理代碼,並將該代碼移動到系統級別。您所要做的就是定義要執行的任務,並將它們添加到適當的分派隊列中。GCD 負責創建所需的線程,並安排任務在這些線程上運行。由於線程管理現在是系統的一部分,GCD 提供了任務管理和執行的整體方法,比傳統線程提供了更高的效率。

     OperationQueue(操作隊列,api 類名為 NSOperationQueue )是 Objective-C 對象,是對 GCD 的封裝。其作用非常類似於分派隊列。您定義要執行的任務,然後將它們添加到 OperationQueue 中, OperationQueue 處理這些任務的調度和執行。與 GCD 一樣, OperationQueue 為您處理所有線程管理,確保在系統上儘可能快速有效地執行任務。

     接下來,就對現在工作中常用的這兩種技術進行比較和實例解析。

GCD、OperationQueue 對比

核心理念

  • GCD的核心概念:將 任務(block) 添加到隊列,並且指定執行任務的函數。
  • NSOperation 的核心概念:把 操作(異步) 添加到 隊列。

區別

  • GCD:

    • 將任務(block)添加到隊列(串行/並發/主隊列),並且指定任務執行的函數(同步/異步)
    • GCD是底層的C語言構成的API
    • iOS 4.0 推出的,針對多核處理器的並發技術
    • 在隊列中執行的是由 block 構成的任務,這是一個輕量級的數據結構
    • 要停止已經加入 queue 的 block 需要寫複雜的代碼
    • 需要通過 Barrier(dispatch_barrier_async)或者同步任務設置任務之間的依賴關係
    • 只能設置隊列的優先級
    • 高級功能:
      dispatch_once_t(一次性執行, 多線程安全);
      dispatch_after(延遲);
      dispatch_group(調度組);
      dispatch_semaphore(信號量);
      dispatch_apply(優化順序不敏感大體量for循環);
  • 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 圖對部分功能進行分析。

GCD

隊列

串行隊列(Serial Queues)

     串行隊列中的任務按順序執行;但是不同串行隊列間沒有任何約束; 多個串行隊列同時執行時,不同隊列中任務執行是並發的效果。比如:火車站買票可以有多個賣票口,但是每個排的隊都是串行隊列,整體並發,單線串行。

     注意防坑:串行隊列創建的位置。比如下面代碼示例中:在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 效果圖:

serialGCD
圖中下載時可順利拖動滾動條,是為了說明下載在子線程,不影響UI交互

log:

第0個 開始
第0個 結束
第1個 開始
第0個 更新圖片
第1個 結束
第2個 開始
第1個 更新圖片
第2個 結束
第3個 開始
第2個 更新圖片
第3個 結束
第3個 更新圖片

      由 log 可知: GCD 切到主線程也需要時間,切換完成之前,指令可能已經執行到下個循環了。但是看起來圖片還是依次下載完成和顯示的,因為每一張圖切到主線程顯示都需要時間。

並發隊列(Concurrent Queues)

     並發隊列依舊保證中任務按加入的先後順序開始(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 和鎖的區別

  • 依賴對象不同,barrier 依賴的對象是自定義並發隊列,鎖操作依賴的對象是線程。
  • 作用不同,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 效果圖:
conGCD

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個結束

串行、並發隊列對比圖

gcd-cheatsheet

注意事項

  • 無論串行還是並發隊列,都是 FIFO ;
    一般創建 任務(blocks)和加任務到隊列是在主線程,但是任務執行一般是在其他線程(asyc)。需要刷新 UI 時,如果當前不再主線程,需要切回主線程執行。當不確定當前線程是否在主線程時,可以使用下面代碼:
/**
 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(塊)相關

     調度隊列複製添加到它們中的塊,並在執行完成時釋放塊。
     雖然隊列在執行小任務時比原始線程更有效,但是創建塊並在隊列上執行它們仍然存在開銷。如果一個塊執行的工作量太少,那麼內聯執行它可能比將它分派到隊列中要便宜得多。判斷一個塊是否工作量太少的方法是使用性能工具為每個路徑收集度量數據並進行比較。
     您可能希望將 block 的部分代碼包含在 @autoreleasepool 中,以處理這些對象的內存管理。儘管 GCD 調度隊列擁有自己的自動釋放池,但它們不能保證這些池何時耗盡。如果您的應用程序是內存受限的,那麼創建您自己的自動釋放池可以讓您以更有規律的間隔釋放自動釋放對象的內存。

dispatch_after

     dispatch_after 函數並不是在指定時間之後才開始執行處理,而是在指定時間之後將任務追加到隊列中。這個時間並不是絕對準確的。
  代碼示例:

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

dispatch_semaphore

      在多線程訪問可變變量時,是非線程安全的。可能導致程序崩潰。此時,可以通過使用信號量(semaphore)技術,保證多線程處理某段代碼時,後麵線程等待前麵線程執行,保證了多線程的安全性。使用方法記兩個就行了,一個是wait(dispatch_semaphore_wait),一個是signal(dispatch_semaphore_signal)。

具體請參考文章Semaphore回顧

dispatch_apply

     當每次迭代中執行工作與其他所有迭代中執行的工作不同,且每個循環完成的順序不重要時,可以用 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次 。

自問自答

  • 一個隊列的不同任務可以在多個線程執行嗎?
    答:串行隊列,異步執行時,只開一個子線程;無所謂多個線程執行;
    並發隊列,異步執行時,會自動開多個線程,可以在多個線程並發執行不同的任務。

  • 一個線程可以同時執行多個隊列的任務嗎?
    答:一個線程某個時間點只能執行一個任務,執行完畢後,可能執行到來自其他隊列的任務(如果有的話)。比如:主線程除了執行主隊列中任務外,也可能會執行非主隊列中的任務。

    隊列與線程關係示例圖:
    queues & threads

  • qualityOfService 和 queuePriority 的區別是什麼?
    答:
    qualityOfService:
         用於表示 operation 在獲取系統資源時的優先級,默認值:NSQualityOfServiceBackground,我們可以根據需要給 operation 賦不同的優化級,如最高優化級:NSQualityOfServiceUserInteractive。
    queuePriority:
         用於設置 operation 在 operationQueue 中的相對優化級,同一 queue 中優化級高的 operation(isReady 為 YES) 會被優先執行。
         需要注意區分 qualityOfService (在系統層面,operation 與其他線程獲取資源的優先級) 與 queuePriority (同一 queue 中 operation 間執行的優化級)的區別。同時,需要注意 dependencies (嚴格控制執行順序)與 queuePriority (queue 內部相對優先級)的區別。

  • 添加依賴後,隊列中網絡請求任務有依賴關係時,任務結束判定以數據返回為準還是以發起請求為準?
    答:以發起請求為準。分析過程詳見NSOperationQueue隊列中操作依賴相關思考

OperationQueue

  • NSOperation
         NSOperation 是一個”抽象類”,不能直接使用。抽象類的用處是定義子類共有的屬性和方法。NSOperation 是基於 GCD 做的面向對象的封裝。相比較 GCD 使用更加簡單,並且提供了一些用 GCD 不是很好實現的功能。是蘋果公司推薦使用的並發技術。它有兩個子類:

    • NSInvocationOperation (調用操作)
    • NSBlockOperation (塊操作)
           一般常用NSBlockOperation,代碼簡單,同時由於閉包性使它沒有傳參問題。任務被封裝在 NSOperation 的子類實例類對象里,一個 NSOperation 子類對象可以添加多個任務 block 和 一個執行完成 block ,當其關聯的所有 block 執行完時,就認為操作結束了。
  • NSOperationQueue
          OperationQueue也是對 GCD 的高級封裝,更加面向對象,可以實現 GCD 不方便實現的一些效果。被添加到隊列的操作默認是異步執行的。

PS:常見的抽象類有:

  • UIGestureRecognizer
  • CAAnimation
  • CAPropertyAnimation

可以實現 非FIFO 效果

通過對不同操作設置依賴,或優先級,可實現 非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 效果圖:
pauseResume

取消操作

  • 一旦添加到操作隊列中,操作對象實際上歸隊列所有,不能刪除。取消操作的唯一方法是取消它。可以通過調用單個操作對象的 cancel 方法來取消單個操作對象,也可以通過調用隊列對象的 cancelAllOperations 方法來取消隊列中的所有操作對象。
  • 更常見的做法是取消所有隊列操作,以響應某些重要事件,如應用程序退出或用戶專門請求取消,而不是有選擇地取消操作。

取消單個操作對象

取消(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.

  • 操作的 QOS 和隊列的 QOS 有何關係?
    A:隊列的 QOS 設置,會自動把較低優先級的操作提升到與隊列相同優先級。(原更高優先級操作的優先級保持不變)。後續添加進隊列的操作,優先級低於隊列優先級時,也會被自動提升到與隊列相同的優先級。
    注意,蘋果文檔如下的解釋是錯誤的 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.
    原因詳見:Can NSOperation have a lower qualityOfService than NSOperationQueue?

常見問題

如何解決資源競爭問題

資源競爭可能導致數據異常,死鎖,甚至因訪問野指針而崩潰。

  • 對於有明顯先後依賴關係的任務,最佳方案是 GCD串行隊列,可以在不使用線程鎖時保證資源互斥。
  • 其他情況,對存在資源競爭的代碼加鎖或使用信號量(初始參數填1,表示只允許一條線程訪問資源)。
  • 串行隊列同步執行時,如果有任務相互等待,會死鎖。
    比如:在主線程上同步執行任務時,因任務和之前已加入主隊列但未執行的任務會相互等待,導致死鎖。
  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");
}

如何提高代碼效率

「西餅傳說」

代碼設計優先級:系統方法 > 並行 > 串行 > 鎖,簡記為:西餅傳說

  • 儘可能依賴 系統 框架。實現並發性的最佳方法是利用系統框架提供的內置並發性。
  • 儘早識別系列任務,並儘可能使它們更加 並行。如果因為某個任務依賴於某個共享資源而必須連續執行該任務,請考慮更改體系結構以刪除該共享資源。您可以考慮為每個需要資源的客戶機製作資源的副本,或者完全消除該資源。
  • 不使用鎖來保護某些共享資源,而是指定一個 串行隊列 (或使用操作對象依賴項)以正確的順序執行任務。
  • 避免使用 GCD 調度隊列操作隊列 提供的支持使得在大多數情況下不需要鎖定。

確定操作對象的適當範圍

  • 儘管可以向操作隊列中添加任意大量的操作,但這樣做通常是不切實際的。與任何對象一樣,NSOperation 類的實例消耗內存,並且具有與其執行相關的實際成本。如果您的每個操作對象只執行少量的工作,並且您創建了數以萬計的操作對象,那麼您可能會發現,您花在調度操作上的時間比花在實際工作上的時間更多。如果您的應用程序已經受到內存限制,那麼您可能會發現,僅僅在內存中擁有數萬個操作對象就可能進一步降低性能。
  • 有效使用操作的關鍵是 在你需要做的工作量和保持計算機忙碌之間找到一個適當的平衡 。盡量確保你的業務做了合理的工作量。例如,如果您的應用程序創建了 100 個操作對象來對 100 個不同的值執行相同的任務,那麼可以考慮創建 10 個操作對象來處理每個值。
  • 您還應該避免將大量操作一次性添加到隊列中,或者避免連續地將操作對象添加到隊列中的速度快於處理它們的速度。與其用操作對象淹沒隊列,不如批量創建這些對象。當一個批處理完成執行時,使用完成塊告訴應用程序創建一個新的批處理。當您有很多工作要做時,您希望保持隊列中充滿足夠的操作,以便計算機保持忙碌,但是您不希望一次創建太多操作,以至於應用程序耗盡內存。
  • 當然,您創建的操作對象的數量以及在每個操作對象中執行的工作量是可變的,並且完全取決於您的應用程序。你應該經常使用像 Instruments 這樣的工具來幫助你在效率和速度之間找到一個適當的平衡。有關 Instruments 和其他可用於為代碼收集度量標準的性能工具的概述,請參閱 性能概述

術語解釋摘錄

  • 異步任務(asynchronous tasks):由一個線程啟動,但實際上在另一個線程上運行,利用額外的處理器資源更快地完成工作。
  • 互斥(mutex):提供對共享資源的互斥訪問的鎖。
    互斥鎖一次只能由一個線程持有。試圖獲取由不同線程持有的互斥對象會使當前線程處於休眠狀態,直到最終獲得鎖為止。
  • 進程(process):應用軟件或程序的運行時實例。
    進程有自己的虛擬內存空間和系統資源(包括端口權限) ,這些資源獨立於分配給其他程序的資源。一個進程總是包含至少一個線程(主線程) ,並且可能包含任意數量的其他線程。
  • 信號量(semaphore):限制對共享資源訪問的受保護變量。
    互斥(Mutexes)和條件(conditions)都是不同類型的信號量。
  • 任務(task),表示需要執行的工作量。
  • 線程(thread):進程中的執行流程。
    每個線程都有自己的堆棧空間,但在其他方面與同一進程中的其他線程共享內存。
  • 運行循環(run loop): 一個事件處理循環,
    接收事件並派發到適當的處理程序。

官方並發編程詞彙表

本文 demo 地址

MultiThreadDemo

參考文章

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

下節預告

文中提到的知識點,「與其用操作對象淹沒隊列,不如批量創建這些對象。當一個批處理完成執行時,使用完成塊告訴應用程序創建一個新的批處理」,在最近的工作中的確有需要類似的需求,等有時間會進行總結,就作為下一篇文章的預告吧。

本文由博客群發一文多發等運營工具平台 OpenWrite 發佈