Swift 并發(fā)編程(一)GCD篇

引言

今天從Books中翻出了“沉淀”已久的關(guān)于并發(fā)編程書(shū),讀完之后,感受頗多,有一些不確定的知識(shí)點(diǎn)更加清晰了。在此,結(jié)合自己的開(kāi)發(fā)經(jīng)驗(yàn),做一個(gè)總結(jié)。主要從兩個(gè)方面入手:GCD和Operation,也是目前比較主流的實(shí)現(xiàn)并發(fā)的方式。

GCD 和 Operation的區(qū)別和抉擇

GCD是基于libdispatch實(shí)現(xiàn)的,目的是為了降低并發(fā)的成本;最大的特點(diǎn)是大量使用了block,使多線程的實(shí)現(xiàn)變得簡(jiǎn)單。Operation是基于GCD的封裝,更具有封裝性,具備GCD不能實(shí)現(xiàn)的功能,比如cancel任務(wù)。

如何選擇?

選用GCD:簡(jiǎn)單任務(wù),且代碼不會(huì)被重用;

選用Operation:代碼適合封裝起來(lái)重用;多層異步嵌套(如異步A回調(diào)B,B回調(diào)C...如果用GCD會(huì)造成多層block嵌套,可讀性太差);有cancel操作。

不要混淆隊(duì)列和Thread

先來(lái)看一個(gè)面試經(jīng)常遇到的考題:

dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_sync(mainQueue, ^{  // 這個(gè)地方會(huì)阻塞主線程
print("hello");
});

首先隊(duì)列不是線程,隊(duì)列是一個(gè)FIFO的數(shù)據(jù)結(jié)構(gòu);創(chuàng)建一個(gè)隊(duì)列,系統(tǒng)會(huì)根據(jù)系統(tǒng)的使用情況創(chuàng)建一個(gè)或者多個(gè)線程來(lái)配合這個(gè)隊(duì)列工作。線程是實(shí)實(shí)在在運(yùn)行邏輯的。你可以抽象成這個(gè)樣子:


1.png

根據(jù)圖來(lái)分析: 主線程執(zhí)行主隊(duì)列中的print("Hello")操作,而且是同步的,此時(shí)主線程等待,主隊(duì)列任務(wù)開(kāi)始執(zhí)行,但是主隊(duì)列的Task需要在主線程完成,這樣就出現(xiàn)了兩者互相等待的情況,所以造成死鎖。所以請(qǐng)一定記住“隊(duì)列不等于線程”

GCD常見(jiàn)用法

  1. 創(chuàng)建同步隊(duì)列

    let label = "com.piplab.queue.identifer"
    let queue = DispatchQueue(label: label)
    

    label是隊(duì)列的唯一標(biāo)識(shí) ,當(dāng)隊(duì)列創(chuàng)建后,OS會(huì)選擇性的為隊(duì)列創(chuàng)建或分配一個(gè)或多個(gè)線程;如果有可復(fù)用的線程,就復(fù)用線程;否則,將根據(jù)情況創(chuàng)建。

    主隊(duì)列比較特殊,App啟動(dòng)時(shí)創(chuàng)建,并且是用來(lái)處理UI相關(guān)操作的同步隊(duì)列。切記永遠(yuǎn)不要同步主隊(duì)列進(jìn)行一些除UI相關(guān)的耗時(shí)操作。

  2. 創(chuàng)建并發(fā)隊(duì)列

    let label = "com.piplab.queue.concurrent"
    let queue = DispatchQueue(label: label, attributes: .current)
    

    除了自定義外,系統(tǒng)已經(jīng)預(yù)定義了6種不同優(yōu)先級(jí)的并發(fā)?隊(duì)列,下面會(huì)講到。

  3. 隊(duì)列優(yōu)先級(jí)

    按照優(yōu)先級(jí)從高到低依次是:

    • .userInteractive: 一般用于用戶交互,需要快速響應(yīng)的情況。
    • .userInitiated: 一般用戶用戶交互之后,需要迅速異步操作的情況,比如用戶需要讀取數(shù)據(jù)庫(kù),需要快速響應(yīng),讀取數(shù)據(jù)。
    • .default: 系統(tǒng)默認(rèn)優(yōu)先級(jí)。不要直接使用,否則會(huì)出現(xiàn)不必要的錯(cuò)誤
    • .utility :一般用于進(jìn)度條,IO,網(wǎng)絡(luò)請(qǐng)求等情況。系統(tǒng)會(huì)根據(jù)電池情況,平衡響應(yīng)頻率
    • .background: 一般用戶用戶不需要感知的情況。
    • .unspecified: 不建議使用。

    這六種優(yōu)先級(jí)并發(fā)隊(duì)列,系統(tǒng)都有預(yù)定義。但是并不意味著我們不能創(chuàng)建不同優(yōu)先級(jí)的隊(duì)列。

    let label = "com.piplab.quality"
    let queue = DispatchQueue(label: label, qos: .userInteractive, attributes: .current)
    

    注意: Queue的優(yōu)先級(jí)并不是一味不變的。如果將一個(gè)高優(yōu)先級(jí)的Task交給一個(gè)低優(yōu)先級(jí)的Queue,那么Queue的優(yōu)先級(jí)會(huì)跟著提升,并且Queue中的其他Task的優(yōu)先級(jí)也會(huì)跟著提升

  4. 派發(fā)任務(wù)

    DispatchQueue.global(qos: .utility).async { [weak self] in
      guard let self = self else { return }
       // do something
      // Switch back to the main queue to
      // update your UI
      DispatchQueue.main.async {
        self.textLabel.text = "New articles available!"
      }
    }
    

    雖然在block中不聲明 [weak self] 也不會(huì)造成循環(huán)引用,但是會(huì)延長(zhǎng)self的聲明周期,直到block執(zhí)行完畢。在切換到主隊(duì)列時(shí),盡量做比較少的工作。

  5. DispatchGroup

    適用于“當(dāng)一組任務(wù)完成后,再執(zhí)行特定的任務(wù),組任務(wù)并發(fā)執(zhí)行”

    let group = DispatchGroup()
    
    someQueue.async(group: group) { ... your work ... }                  //任務(wù)1
    someQueue.async(group: group) { ... more work .... }                 //任務(wù)2
    someOtherQueue.async(group: group) { ... other work ... }    //任務(wù)3
    
    group.notify(queue: DispatchQueue.main) { [weak self] in
       self?.textLabel.text = "All jobs have completed"                  //任務(wù)4
    }
    

    值的注意的是一個(gè)group里的Task,可以分配到不同的Queue。 任務(wù)4會(huì)在任務(wù)1,2,3 都執(zhí)行完之后再執(zhí)行,如果任務(wù)1,2,3中有一直執(zhí)行不完,那么任務(wù)4是不會(huì)執(zhí)行的。

    如果不能等待所有的任務(wù)執(zhí)行完,還可以用以下方式實(shí)現(xiàn):

    let group = DispatchGroup()
    
    someQueue.async(group: group) { ... }
    someQueue.async(group: group) { ... }
    someOtherQueue.async(group: group) { ... } 
    
    if group.wait(timeout: .now() + 60) == .timedOut {
      print("The jobs didn't finish in 60 seconds")
    } else {
      print ("All jobs have complete")
    }
    

    切記不要在主線程調(diào)用group.wait方法.

    對(duì)于多層異步嵌套的情況,group還有另外一種實(shí)現(xiàn)方式:

    queue.dispatch(group: group) {
      // count is 1
      group.enter()
      // count is 2
      someAsyncMethod { 
        defer { group.leave() }
        
        // Perform your work here,
        // count goes back to 1 once complete
      }
    }
    
  6. 信號(hào)量Semaphores

    簡(jiǎn)單的理解信號(hào)量 就是一個(gè)計(jì)數(shù)器,每次使用時(shí) -1 ,為0時(shí),代表資源不夠,線程等待,釋放時(shí) +1 。一個(gè)簡(jiǎn)單的例子: 批量下載圖片,最多4個(gè)線程同時(shí)下載,那么4可以定義為一個(gè)信號(hào)量。

    let semaphore = DispatchSemaphore(value: 4)
    semaphore.wait()  //信號(hào)量-1,如果為0,則等待
    semaphore.signal() //信號(hào)量 +1
    
  7. 并發(fā)三大難題

    • 資源競(jìng)爭(zhēng):在多個(gè)線程“同時(shí)寫(xiě)” 的情況,容易出現(xiàn)錯(cuò)誤。
      2.png
 假設(shè)有一個(gè)值value為1,別用兩個(gè)線程各使value的值+1,正確結(jié)果應(yīng)該是3,但實(shí)際效果是:value的值為1,第一個(gè)時(shí)鐘,Thread1讀取的值是1;第二個(gè)時(shí)鐘Thread1將value的值修改為2,因?yàn)門(mén)hread1還沒(méi)有寫(xiě)回到value中,所以Thread2讀取的值是1,第三個(gè)時(shí)鐘Thread1將2寫(xiě)回到value中,thread2將value值+1修改為2,第四個(gè)時(shí)鐘,Thread2將2寫(xiě)回到value中,這樣結(jié)果為2,所以是錯(cuò)誤的。正確的做法是將“+1”操作和寫(xiě)操作同步執(zhí)行。

 `serialQueue.async{ value +1 ,write()}`

  下面說(shuō)一下并發(fā)情況下變量訪問(wèn)安全措施(這是另外一個(gè)問(wèn)題,跟上面無(wú)關(guān)哦)。

 當(dāng)多個(gè)線程訪問(wèn)同意變量時(shí),為了避免讀的同時(shí)寫(xiě)的問(wèn)題,如圖:
3.png

實(shí)現(xiàn)如下:

 ```swift
 private let threadSafeCountQueue = DispatchQueue(label: "...", attributes: .concurrent)
 private var _count = 0
 public var count: Int {
   get {
     return threadSafeCountQueue.sync {
       return _count
     }
   }
   set {
     threadSafeCountQueue.async(flags: .barrier) { [unowned self] in
       self._count = newValue
     }
   }
 }
 ```
  • 死鎖問(wèn)題:簡(jiǎn)而言之就是TaskA擁有資源A等待資源B,TaskB 擁有資源B,等待資源A,兩者互相等待,誰(shuí)也拿不到想要的資源。形象點(diǎn)表示,如下圖:


    4.png

    避免這個(gè)問(wèn)題的方法只有一個(gè):每個(gè)線程按照統(tǒng)一的順序申請(qǐng)資源。比如:TaskA需要資源1和資源2,那么TaskB也按照同樣的順序申請(qǐng)資源1和資源2,就不會(huì)出現(xiàn)這個(gè)問(wèn)題。

  • 優(yōu)先級(jí)反轉(zhuǎn)

    這種情況表現(xiàn)為“低優(yōu)先級(jí)的Task擁有高優(yōu)先級(jí)需要的資源,導(dǎo)致高優(yōu)先級(jí)不能執(zhí)行”。試想一下,一個(gè)不著急上廁所的人,拿著所有的紙,即使你再著急,你也沒(méi)辦法啊。造成的原因是“一個(gè)低優(yōu)先級(jí)的隊(duì)列的優(yōu)先級(jí)比高優(yōu)先級(jí)隊(duì)列的優(yōu)先級(jí)還高”。還記得創(chuàng)建Queue的時(shí)候有個(gè)優(yōu)先級(jí)的參數(shù)嗎?DispatchQueue(..., qos: .userInteractive,...) ,還有分配任務(wù)時(shí)也有一個(gè)優(yōu)先級(jí)參數(shù)queue.async(..., qos: .userInteractive,...)``,如果任務(wù)的優(yōu)先級(jí)高于隊(duì)列的優(yōu)先級(jí),就可能出現(xiàn)這種情況。解決的方法很簡(jiǎn)單:“任務(wù)的優(yōu)先級(jí)不能高于隊(duì)列”

最后

相信GCD在大家的日常開(kāi)發(fā)中經(jīng)常用到,一些常見(jiàn)的用法也早已掌握。文中提到的一些注意點(diǎn)是我個(gè)人曾經(jīng)遇到的坎兒,希望對(duì)大家有所幫助。如果有不對(duì)的地方,歡迎留言指正。不知不覺(jué)已經(jīng)凌晨了,先到這兒,明天寫(xiě)一下Operation。最后悄悄寫(xiě)一句“如果喜歡,歡迎打賞”

最后編輯于
?著作權(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)容