iOS-你真的了解并發嗎?

jopen 10年前發布 | 21K 次閱讀 并發 iOS開發 移動開發

翻譯了一篇appcoda的文章,通俗易懂,理清了并發的相關知識點。

原文鏈接: http://www.appcoda.com/ios-concurrency/

并發一直被認為是iOS開發中的比較奇特的一部分。它被認為是危險的所以許多開發者盡可能避免使用。還有傳言說盡可能避免使用多線程。如果你不能很好的理解多線程,多線程的確是危險的。猜想一下人們的生活中有多少危險的行為和活動,很多是吧?只有當我們掌握它,才能運用自如。并發是一個雙刃劍,所以你必須掌握如何使用它。它幫助你寫出高效、快速執行、響應迅速的app,但與此同時,如果誤用則會毫不留情的毀壞你的app。這就是我們在寫并發代碼之前,首先考慮為什么需要并發,哪個API才能解決問題的原因。 在iOS里面我們有許多API可以使用,在這個教程里面,我們來談談最常用的 - NSOperation 和 Dispatch Queues

為什么需要并發?

我知道你是一個很棒的iOS開發者。無論你打算創造什么類型的app,你都需要知道并發能讓你的app能夠響應更迅速并且運行更快。這里總結了使用并發的幾個優點

  • 利用iOS設備硬件:現在全部iOS設備有多核處理器允許開發者并發執行多個任務。你應該利用這個特性發揮硬件的優勢。

  • 更好的體驗:你也許曾經寫過web服務、處理一些IO,或者運行一些繁重的任務。做這類型的操作在UI線程將堵塞app的運行。用戶面對這種情況,直接關閉app。并發在處理這些任務的時候可以在后臺運行,不會阻塞主線程,不會讓用戶感覺的厭煩,他們仍然可以點擊按鈕,滑動app里面的視圖,所有繁重的任務都在后臺運行了。

  • NSOperation和dispatch queues的使用讓并發更加簡單:創建和管理線程并不是一件簡單的任務。這就是為什么大部分開發者懼怕聽到并發和多線程。在iOS里面,并發的API使用起來非常簡單。你不必要關心線程的創建或者是底層的管理。API會幫你做這些事。另外一個重要的優點非常容易實現同步避免資源競爭。競爭發生在多線程訪問共享資源,這樣會導無法預料的結果。通過同步,你保證了線程間的資源的共享。

關于并發你需要知道什么?

這這個教程里,我們將向你解釋需要了解的并發知識,減輕你的恐懼。首先我們推薦先了解一些 blocks (Swift中的閉包),因為并發的API里面大量的使用這些知識。然后我們將談談 dispatch queues 和 NSOperationQueues ,我們將引導你了解并發的知識點、不同點、如何去使用。

Part 1: GCD (Grand Central Dispatch)

GCD 是最常用的API用來管理并發和執行異步操作在Unix底層系統中。GCD提供管理任務的隊列。首先我們來看看隊列(queue)是什么?

什么是Queues

隊列是一個數據結構,管理對象的先進先出(FIFO)。 隊列非常像電影院售票窗口排隊情況。 誰先來舍就能買到票。在計算機科學中,第一個添加進隊列的對象也是第一個被移除的。

Dispatch Queues

Dispatch queues 能夠非常容易在你的應用中異步的并發執行任務。任務以 blocks 的形式提交到隊列中。有兩種類型的隊列:

  • (1) 串行隊列(serial queues)
  • (2) 并發隊列(concurrent queues)

在談他們之間的不同點時,你應該知道任務是不同線程中執行而不是單獨的線程中處理。換句話說,你在主線程中創建的blocks提交了任務到dispatch queues.但是所有這些任務(Blocks of codes)將在不同的線程中執行。

Serial Queues

當你創建一個串行隊列的時候,這個隊列同一時間只能執行一個任務.所有在隊列的任務都是平等的,按順序執行。當然,你不必關心不同隊列的任務,因為你仍然可以通過多個串行隊列并發執行任務。舉個例子:你可以創建兩個串行隊列,每個隊列同一時刻執行一個任務,但是兩個任務可以并發執行。

串行隊列管理共享資源是非常有用的。他保證按順序訪問,防止競爭。想象一下只有售票口,但是一群人都想去買,所有的工作人員在這里是共享資源。如果工作人員不得不同時給人們提供服務,那么這將會變得沒有秩序。為了解決這鐘情況,需要要求人們排隊(serial queue),所以工作人員才能同一時間為消費者提供服務。

另外一方面,這并不意味著電影院可以同一時間接納一個消費者,假如它設定了多個售票口,他便可以為多個消費者服務。這就是我說的為什么使用多個串行隊列任然可以執行多個任務。

使用串行隊列的有點:

  • 1、保證訪問的有序性,防止資源的競爭。
  • 2、任務有序的執行。提交到串行隊列的任務將按順序執行
  • 3、可以創建多個串行隊列

Concurrent Queues

顧名思義,并發隊列允許并發的執行多個任務。每個任務(blocks of codes)按照他們添加到隊列中的順序啟動。但是他們的執行是并發的,他們不必等其他任務開始。并發隊列保證任務同一時間開始但不知道執行的順序,執行時間、同一個時間點執行的任務數量。

舉個列子,你提交了3個任務(#1, #2, #3)到一個并發隊列.任務是并發執行的,啟動的時候是按照添加到隊列的順序啟動。然后,執行時間和完成時間是不一樣的。有可能 #2 和 #3 開始花費了一些時間,也有可能在 #1 任務完成前啟動。 由系統來決定執行這些任務。

使用Queues

剛剛我們解釋了串行和并發隊列,現在我們來看看怎么使用它們。默認情況下,系統提供給我們一個串行隊列和4個并發隊列。

main dispatch queue 是全局的串行隊列在主線程中執行。用來更新APP UI和運行UIView相關的更新。同一時間只有一個任務被執行,這就是當我們運行繁重的任務時會阻塞UI

除了主隊列,系統還提供4個并發隊列。我們稱他們是 Global Dispatch queues 。這些隊列是全局的,通過優先級來區分。使用這些全局的并發隊列,你可以獲得首選隊列使用 dispatch_get_global_queue ,其中第一個參數可以有下面幾個值:

  • DISPATCH_QUEUE_PRIORITY_HIGH
  • DISPATCH_QUEUE_PRIORITY_DEFAULT
  • DISPATCH_QUEUE_PRIORITY_LOW
  • DISPATCH_QUEUE_PRIORITY_BACKGROUND

這些隊列類型代表著執行的優先級。 DISPATCH_QUEUE_PRIORITY_HIGH 優先級最高, DISPATCH_QUEUE_PRIORITY_BACKGROUND 優先級最低。所以你可以基于任務的優先級來使用這些隊列。請注意,Apple的API也在使用這些隊列,所以你的任務并不是唯一的在這些隊列中。

最后,你可以創建任意數量的串行和并行隊列。盡管你可以自己創建并發隊列,但我強烈建議使用這4個全局的并發隊列。

GCD 備忘錄

現在,你已經基本了解了 dispatch queues ,我打算給你一個簡單的備忘錄,這個圖非常簡單,包含了所有你需要知道的GCD知識點。

很棒是吧?現在讓我們來寫一個demo來看看如何使用dispatch queues。我講向你展示如何利用dispatch queues完善app的展示,讓它更快的響應。

Demo Project

這個demo非常簡單,展示4張圖片,每一個需要請求網絡。這個圖片在主線程請求。為了展示給你UI是如何響應的,我添加了一個slider在image下面。 下載Demo

點擊start按鈕下載圖片,與此同時拖動slider在下載期間,你會發現你不能拖動他。

當你點擊開始按鈕,圖片開始被加載在主線程。很明顯,這個方法非常糟糕,讓UI沒有反應。不幸的是現在還有好多APP在加載這樣的任務的時候還放在主線程。現在我們用dispatch queues修改它。

首先我們將使用并發隊列然后再使用串行隊列

使用并發隊列

打開 ViewController.swift ,在 didClickOnStart 方法是處理圖片的下載。

@IBAction func didClickOnStart(sender: AnyObject) {

let img1 = Downloader.downloadImageWithURL(imageURLs[0])
self.imageView1.image = img1

let img2 = Downloader.downloadImageWithURL(imageURLs[1])
self.imageView2.image = img2

let img3 = Downloader.downloadImageWithURL(imageURLs[2])
self.imageView3.image = img3

let img4 = Downloader.downloadImageWithURL(imageURLs[3])
self.imageView4.image = img4

}

每一個 downloader 被認為是一個任務,所有任務都在主線程執行。現在,讓我們獲得一個全局并發隊列的引用,這個優先級的Default

let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
       dispatch_async(queue) { () -> Void in

           let img1 = Downloader.downloadImageWithURL(imageURLs[0])
           dispatch_async(dispatch_get_main_queue(), {

               self.imageView1.image = img1
           })

       }

我們在block里面提交了一個任務下載第一張圖片.當圖片下載完成,我們提交另一個任務給主線程,使用這個下載好的image更新視圖. 換句話說,我們把這些任務放到后臺線程下載,然后在主線程執行UI相關的操作. didClickOnStart 修改后的代碼:

 @IBAction func didClickOnStart(sender: AnyObject) {

    let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
    dispatch_async(queue) { () -> Void in

        let img1 = Downloader.downloadImageWithURL(imageURLs[0])
        dispatch_async(dispatch_get_main_queue(), {

            self.imageView1.image = img1
        })

    }
    dispatch_async(queue) { () -> Void in

        let img2 = Downloader.downloadImageWithURL(imageURLs[1])

        dispatch_async(dispatch_get_main_queue(), {

            self.imageView2.image = img2
        })

    }
    dispatch_async(queue) { () -> Void in

        let img3 = Downloader.downloadImageWithURL(imageURLs[2])

        dispatch_async(dispatch_get_main_queue(), {

            self.imageView3.image = img3
        })

    }
    dispatch_async(queue) { () -> Void in

        let img4 = Downloader.downloadImageWithURL(imageURLs[3])

        dispatch_async(dispatch_get_main_queue(), {

            self.imageView4.image = img4
        })
    }

}

你剛剛提交了4個并發下載圖片的任務在 default queue 。現在運行app,它將快速的響應,并且下載的過程中還能夠拖動slider

使用串行隊列

另外一種解決的辦法是使用串行隊列。回到剛剛的 didClickOnStart() 方法,現在我們將使用串行隊列來下載圖片. 當我們使用串行隊列的時候,我們應該注意引用了那一條隊列。每個app有一個默認的串行隊列(就是main queue)。 所以,我們在使用串行隊列的時候,必須創建一個新的隊列,否則任務可能在更新UI的時候執行,這會帶來很糟糕的用戶體驗。你可以使用 dispatch_queue_create 來創建一個新的隊列,修改后的代碼是這樣的:

 @IBAction func didClickOnStart(sender: AnyObject) {

    let serialQueue = dispatch_queue_create("com.appcoda.imagesQueue", DISPATCH_QUEUE_SERIAL)


    dispatch_async(serialQueue) { () -> Void in

        let img1 = Downloader .downloadImageWithURL(imageURLs[0])
        dispatch_async(dispatch_get_main_queue(), {

            self.imageView1.image = img1
        })

    }
    dispatch_async(serialQueue) { () -> Void in

        let img2 = Downloader.downloadImageWithURL(imageURLs[1])

        dispatch_async(dispatch_get_main_queue(), {

            self.imageView2.image = img2
        })

    }
    dispatch_async(serialQueue) { () -> Void in

        let img3 = Downloader.downloadImageWithURL(imageURLs[2])

        dispatch_async(dispatch_get_main_queue(), {

            self.imageView3.image = img3
        })

    }
    dispatch_async(serialQueue) { () -> Void in

        let img4 = Downloader.downloadImageWithURL(imageURLs[3])

        dispatch_async(dispatch_get_main_queue(), {

            self.imageView4.image = img4
        })
    }

}

需要注意的兩個點:

  • 1、比起并發隊列下載,串行隊列花費長一點的時間下載圖片。原因是因為我們同一時間只下載一張圖片。每一個任務需要等待先前的任務完成,然后再執行
  • 2、image是按順序加載的,image1,image2,image3,image4。這是因為串行隊列在同一時間只能執行一個任務

Part 2: Operation Queues

GCD的底層是基于C來實現并發的。 Operation queues ,是基于GCD的抽象。這意味著可以像GCD一樣執行任務,但是有面向對象的風格。總之,Operation queues讓開發者使用起來更簡單。

不像 GCD , Operation queues 不遵循先進先出順序。 Operation queues 和 dispatch queues 的不同點是:

  • 1、不遵從FIFO(先進先出): 在 Operation queues 可以設置operation的優先級、添加operation間的依賴,這意味著可以定義operation的執行順序,某些operation必須在其他opeartion完成之后執行。這就沒有遵從先進先出的概念.

  • 2、默認的,operation是并發的: 不能改變成串行類型。但是能夠通過設置operation之間的依賴關系來實現串行的功能。

  • 3、Operation Queues是 NSOperationQueue 的實例,任務封裝在 NSOperation 實例當中

NSOperation

提交到Operation Queues的任務都是 NSOperation 實例。 我們在GCD中討論過任務提交的形式都是通過block。 這里也可以這樣做,只不過打包在 NSOperation 中. 你可以簡單的認為 NSOperation 是工作的一個單元。

NSOperation 是一個抽象的class,不能直接使用,所以我們不得不使用使用它的子類.在iOS中提供了兩個現有的 NSOperation 的子類,這些類可以直接使用,但你任然可以使用自己的 NSOperation 子類運行這些操作,這兩個類是:

  • 1、NSBlockOperation:使用這個類用block形式初始化operation. 這個operation自己可以包含多個block,當所有block執行完畢,這個operation則被認為是完成

  • 2、NSInvocationOperation: 用invok一個selector的方式初始化一個operation

那么,NSOperation的有點是什么?

  • 1、首先,他們支持依賴關系,通過使用 addDependency 方法.當你需要開始一個operation依賴于另外一個operation執行完成時候,會使用NSOperation。

  • 2、可以改變執行的優先級,設置 queuePriority 屬性為下面的值,高優先級的會先執行。

public enum NSOperationQueuePriority : Int {
   case VeryLow
   case Low
   case Normal
   case High
   case VeryHigh
}
  • 3、可以取消特定的operation或者所有operation.operation添加到queue之后可以被取消, cancel() 方法被調用的時候取消已經完成。 當你取消任意一個operation,下面三個當中場景的其中一個會發生:

    • operation已經完成。這種情況cancel方法什么都不做。
    • operation正在執行.這種情況,系統不會強制停止operation,但會吧 cancelled 屬性置為true
    • operation任然在等待執行。這種情況,operation將永遠不會被執行。
  • 4、NSOperation有3個有用的bool屬性 finished 、 cancelled 、 ready .

    • finished 在執行完成后設置為true
    • cancelled 在operation被調用 cancel() 方法后設置為true
    • ready 在operation即將執行的時候設置true
  • 5、任何一個 NSOperation 有一個完成后被調用的block,這個block將被調用,當finished設置為true的時候

現在讓我們用NSOperationQueues重寫剛剛那個的demo。首先聲明屬性,在ViewController里面:

var queue = NSOperationQueue()

在 didClickOnstart() 方法里面修改代碼:

 @IBAction func didClickOnStart(sender: AnyObject) {
    queue = NSOperationQueue()

    queue.addOperationWithBlock { () -> Void in

        let img1 = Downloader.downloadImageWithURL(imageURLs[0])

        NSOperationQueue.mainQueue().addOperationWithBlock({
            self.imageView1.image = img1
        })
    }

    queue.addOperationWithBlock { () -> Void in
        let img2 = Downloader.downloadImageWithURL(imageURLs[1])

        NSOperationQueue.mainQueue().addOperationWithBlock({
            self.imageView2.image = img2
        })

    }

    queue.addOperationWithBlock { () -> Void in
        let img3 = Downloader.downloadImageWithURL(imageURLs[2])

        NSOperationQueue.mainQueue().addOperationWithBlock({
            self.imageView3.image = img3
        })

    }

    queue.addOperationWithBlock { () -> Void in
        let img4 = Downloader.downloadImageWithURL(imageURLs[3])

        NSOperationQueue.mainQueue().addOperationWithBlock({
            self.imageView4.image = img4
        })

    }
}

使用 addOperationWithBlock 創建了一個operation,很簡單是不?為了在主線程執行,替代了使用GCD中調用的 dispatch_async() ,我們可以用 NSOperationQueue.mainQueue() 實現相同的功能。

上面的例子使用 addOperationWithBlock 來添加operation在queue。讓我們來看看 NSBlockOperation 是如何實現的

@IBAction func didClickOnStart(sender: AnyObject) {

    queue = NSOperationQueue()
    let operation1 = NSBlockOperation(block: {
        let img1 = Downloader.downloadImageWithURL(imageURLs[0])
        NSOperationQueue.mainQueue().addOperationWithBlock({
            self.imageView1.image = img1
        })
    })

    operation1.completionBlock = {
        print("Operation 1 completed")
    }
    queue.addOperation(operation1)

    let operation2 = NSBlockOperation(block: {
        let img2 = Downloader.downloadImageWithURL(imageURLs[1])
        NSOperationQueue.mainQueue().addOperationWithBlock({
            self.imageView2.image = img2
        })
    })

    operation2.completionBlock = {
        print("Operation 2 completed")
    }
    queue.addOperation(operation2)


    let operation3 = NSBlockOperation(block: {
        let img3 = Downloader.downloadImageWithURL(imageURLs[2])
        NSOperationQueue.mainQueue().addOperationWithBlock({
            self.imageView3.image = img3
        })
    })

    operation3.completionBlock = {
        print("Operation 3 completed")
    }
    queue.addOperation(operation3)

    let operation4 = NSBlockOperation(block: {
        let img4 = Downloader.downloadImageWithURL(imageURLs[3])
        NSOperationQueue.mainQueue().addOperationWithBlock({
            self.imageView4.image = img4
        })
    })

    operation4.completionBlock = {
        print("Operation 4 completed")
    }
    queue.addOperation(operation4)
}

創建了一個 NSBlockOperation 實例把任務封裝到block。 使用 NSBlockOperation ,允許設置完成的回調(completion handler). Operation完成后,回調會被調用。運行demo后,控制臺上輸出:

Operation 1 completed
Operation 3 completed
Operation 2 completed
Operation 4 completed

取消Operations

如上面提到的, NSBlockOperation 允許我們管理operations。現在我們創建一個cancel按鈕放到導航欄,實現cancel這個事件。 為了說明cancel operation,添加依賴在 #2 和 #1 之間, 然后在添加 #3 和 #2 之間的依賴。 這就說明 #2 會在 #1 完成時候開始, #3 會在 #2 完成后開始。 #4 沒有依賴會并發工作。

取消所有operation的方法是 cancelAllOperations() ,在ViewController類里面添加。

@IBAction func didClickOnCancel(sender: AnyObject) {

       self.queue.cancelAllOperations()
   }

修改后的 didClickOnStart() 代碼是

@IBAction func didClickOnStart(sender: AnyObject) {
      queue = NSOperationQueue()

      let operation1 = NSBlockOperation(block: {
          let img1 = Downloader.downloadImageWithURL(imageURLs[0])
          NSOperationQueue.mainQueue().addOperationWithBlock({
              self.imageView1.image = img1
          })
      })

      operation1.completionBlock = {
          print("Operation 1 completed, cancelled:\(operation1.cancelled)")
      }
      queue.addOperation(operation1)

      let operation2 = NSBlockOperation(block: {
          let img2 = Downloader.downloadImageWithURL(imageURLs[1])
          NSOperationQueue.mainQueue().addOperationWithBlock({
              self.imageView2.image = img2
          })
      })
      operation2.addDependency(operation1)
      operation2.completionBlock = {
          print("Operation 2 completed, cancelled:\(operation2.cancelled)")
      }
      queue.addOperation(operation2)


      let operation3 = NSBlockOperation(block: {
          let img3 = Downloader.downloadImageWithURL(imageURLs[2])
          NSOperationQueue.mainQueue().addOperationWithBlock({
              self.imageView3.image = img3
          })
      })
      operation3.addDependency(operation2)

      operation3.completionBlock = {
          print("Operation 3 completed, cancelled:\(operation3.cancelled)")
      }
      queue.addOperation(operation3)

      let operation4 = NSBlockOperation(block: {
          let img4 = Downloader.downloadImageWithURL(imageURLs[3])
          NSOperationQueue.mainQueue().addOperationWithBlock({
              self.imageView4.image = img4
          })
      })

      operation4.completionBlock = {
          print("Operation 4 completed, cancelled:\(operation4.cancelled)")
      }
      queue.addOperation(operation4)

  }

執行的結果:

Operation 3 completed, cancelled:true
Operation 2 completed, cancelled:true
Operation 1 completed, cancelled:true
Operation 4 completed, cancelled:true

按下start按鈕后點擊取消按鈕. 這些operation將被取消在operation #1 完成之后。

在這里發生了什么?

  • 1、當 #1 已經執行,cancel不執行任何事情。第一張圖片任然顯示

  • 2、點擊cancel按鈕足夠快, #2 被取消. cancelAllOperations() 阻止 #2 執行,所以image2沒有顯示

  • 3、 #3 依賴 #2 完成,因為 #2 取消了,所以 #2 也不能被執行

  • 4、 #4 沒有依賴,并發執行,所以下載了圖片.

總結

1、了解并發的概念、解釋了GCD、創建串行和并發隊列

2、檢驗了NSOperationQueues的執行順序

3、學習了NSOperationQueues和GCD的優缺點

參考

完整的 Demo

深入了解: https://developer.apple.com/library/ios/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Introduction/Introduction.html

來自: http://www.liuchendi.com/2016/01/05/iOS/29_GCD_Operation/

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!