iOS多執行緒之GCD、OperationQueue 對比和實踐記錄

Dast1發表於2020-08-27

簡介

     在計算的早期,計算機可以執行的最大工作量是由 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 效果圖:

圖中下載時可順利拖動滾動條,是為了說明下載在子執行緒,不影響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 效果圖:

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

序列、併發佇列對比圖

注意事項

  • 無論序列還是併發佇列,都是 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次 。

自問自答

  • 一個佇列的不同任務可以在多個執行緒執行嗎?
    答:序列佇列,非同步執行時,只開一個子執行緒;無所謂多個執行緒執行;
    併發佇列,非同步執行時,會自動開多個執行緒,可以在多個執行緒併發執行不同的任務。

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

    佇列與執行緒關係示例圖:

  • 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 效果圖:

取消操作

  • 一旦新增到操作佇列中,操作物件實際上歸佇列所有,不能刪除。取消操作的唯一方法是取消它。可以通過呼叫單個操作物件的 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

相關文章