iOS開發之多執行緒程式設計總結(二)

發表於2016-11-12

背景

擔心了兩週的我終於輪到去醫院做胃鏡檢查了!去的時候我都想好了最壞的可能(胃癌),之前在網上查的症狀都很相似。最後檢查結果出來終於安心了,診斷結果:慢性非萎縮性胃炎(胃竇為主)

我是一個心裡素質不過關的人,所以說對待問題的時候可能會有一種悲觀的想法。朋友說我本來可能沒病都被自己嚇出病了,這是一個心態問題。

你們可能問我做胃鏡什麼感覺?我只能告訴你一個字:真爽,具體只能自己去感受。

11970779-ed8e897d5826ecc9
自己眼中的自己.jpg

保持樂觀的心態

看完下面的笑話就要開始我們的裝逼之旅了^_^

  1. 一個大學生去公司實習,老闆讓他先從掃地開始。大學生:“我可是大學生哎……”老闆:“哦,對了,我差點忘了你是大學生,來來來,我教你怎麼掃地”
  2. 一天,老師讓同學們寫作文,題目是 《我的理想》。
    小明在作文裡寫道:我長大了要去搶銀行,然後把錢分給窮苦老百姓。
    第二天老師改完了,寫給小明的評語是這樣的:很不錯的理想,分錢的時候不要忘了老師,但你要注意你的同桌,他說他長大了要去當警察。

GCD基本介紹

  • GCD(Grand Central Dispatch)iOS 4.0開始引入的新多執行緒程式設計功能,
  • GCD(Grand Central Dispatch)是非同步執行任務的技術之一。一般將應用程式中記述的執行緒管理用的程式碼在系統級中實現。開發者只需要定義想執行的任務並追加到適當的Dispatch Queue中,GCD就能生成必要的執行緒並計劃執行任務。
  • GCD(Grand Central Dispatch)是基於C語言開發的一套多執行緒開發機制,是完全程式導向的。

GCD基本概念

這就需要上一篇部落格裡的基本知識了(不清楚去看下)iOS開發之多執行緒程式設計總結(一)

任務和佇列

  • 任務:就是執行操作的意思,換句話說就是你線上程中執行的那段程式碼。在GCD中是放在block中的。執行任務有兩種方式:同步執行非同步執行。兩者的主要區別是:是否具備開啟新執行緒的能力。
    1. 同步執行(sync):只能在當前執行緒中執行任務,不具備開啟新執行緒的能力
      • 必須等待當前語句執行完畢,才會執行下一條語句
      • 不會開啟執行緒
      • 在當前主執行緒執行 block 的任務
      • dispatch_sync(queue, block);
    2. 非同步執行(async):可以在新的執行緒中執行任務,具備開啟新執行緒的能力
      • 不用等待當前語句執行完畢,就可以執行下一條語句
      • 會開啟執行緒執行 block 的任務
      • 非同步是多執行緒的代名詞
      • dispatch_async(queue, block);
  • 佇列:這裡的佇列指任務佇列,即用來存放任務的佇列。佇列是一種特殊的線性表,採用FIFO(先進先出)的原則,即新任務總是被插入到佇列的末尾,而讀取任務的時候總是從佇列的頭部開始讀取。每讀取一個任務,則從佇列中釋放一個任務。在GCD中有四種佇列:序列佇列併發佇列主佇列全域性佇列
    1. 序列佇列(Serial Dispatch Queue):讓任務一個接著一個地執行(一個任務執行完畢後,再執行下一個任務)
      • 一次只能”排程”一個任務
      • dispatch_queue_create("queue", NULL);
        或者dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    2. 併發佇列(Concurrent Dispatch Queue):可以讓多個任務併發(同時)執行(自動開啟多個執行緒同時執行任務),
      • 一次可以”排程”多個任務
      • 併發功能只有在非同步(dispatch_async)函式下才有效
      • dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);
    3. 主佇列
      • 專門用來在主執行緒上排程任務的佇列
      • 不會開啟執行緒
      • 在主執行緒空閒時才會排程佇列中的任務在主執行緒執行
      • dispatch_get_main_queue();
    4. 全域性佇列
      • 執行過程和併發佇列一致,參考併發佇列
      • dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

小結: 在以後的使用中,記住下面的就可以了!

  1. 開不開執行緒由執行任務的函式決定
    • 非同步開,非同步是多執行緒的代名詞
    • 同步不開
  2. 開幾條執行緒由佇列決定
    • 序列佇列開一條執行緒(GCD會開一條,NSOperation Queue最大併發數為1時也可能開多條)
    • 併發佇列開多條執行緒,具體能開的執行緒數量由底層執行緒池決定

GCD的使用

今天我們學習下面圖片的相關知識點,Demo下載連結會在文章最後給出來

12970779-13fc7deb64f5ac76
GCD知識點.png

簡單來看一段程式碼:非同步下載圖片

NSThread對比可以發現

  • 所有的程式碼寫在一起的,讓程式碼更加簡單,易於閱讀和維護
  • NSThread 通過 @selector 指定要執行的方法,程式碼分散
  • GCD 通過 block 指定要執行的程式碼,程式碼集中
  • 使用 GCD 不需要管理執行緒的建立/銷燬/複用的過程!程式設計師不用關心執行緒的生命週期
  • 如果要開多個執行緒 NSThread 必須例項化多個執行緒物件或者使用分類方法
  • NSThread 靠 NSObject 的分類方法實現的執行緒間通訊,GCD 靠 block
  • dispatch_async(queue, block);就是非同步執行一個佇列裡面的任務block。每個block之間是非同步執行的,但是block裡面的程式碼是順序執行的!
  • dispatch_sync(queue, block);就是同步執行一個佇列裡面的任務block

1. 序列佇列(Serial Dispatch Queue)

序列佇列的建立:

序列佇列同步和非同步執行Demo:

序列佇列 同步執行結果:

序列佇列 非同步執行結果:

小結:
  • 從序列佇列同步執行結果看出列印是交替執行的!從列印中看到number=1,說明執行緒block任務是在主執行緒中執行的。因為同步是不會開闢執行緒的,所有當前只有一個主執行緒MainThread。也就是說序列佇列同步執行不會開闢執行緒,所有block任務之間是同步執行的
  • 從序列佇列非同步執行結果看出列印並不是交替執行的!從列印中看到number=5,說明執行緒block的任務是在一個全新的執行緒中執行的。因為非同步是會開闢執行緒的,所有當前有主執行緒MainThread和子執行緒number=5。也就是說序列佇列非同步執行會僅會開闢一個新的執行緒,所有block任務之間是同步執行的
  • 以先進先出的方式,順序排程佇列中的任務執行
  • 無論佇列中指定的任務函式是同步還是非同步,都會等待前一個任務執行完畢以後,再排程後面的任務

2. 併發佇列(Concurrent Dispatch Queue)

併發佇列建立

併發佇列同步和非同步執行Demo

併發 佇列同步執行結果:

併發佇列 非同步執行結果:

小結:
  • 併發佇列同步執行和序列佇列同步執行一樣,都不會開闢新執行緒,block任務之間是同步執行的!
  • 併發佇列非同步執行結果中看到開闢了多個執行緒,並且執行順序也不是順序執行。因為非同步開多執行緒的代名詞,併發是開多條執行緒的代名詞
  • 有多個執行緒,操作進來之後它會將這些佇列安排在可用的處理器上,同時保證先進來的任務優先處理。
  • 以先進先出的方式,併發排程佇列中的任務執行
  • 如果當前排程的任務是同步執行的,會等待任務執行完成後,再排程後續的任務
  • 如果當前排程的任務是非同步執行的,同時底層執行緒池有可用的執行緒資源,會再新的執行緒排程後續任務的執行

3. 全域性佇列(Global Dispatch Queue)

全域性佇列基本知識
  • dispatch_get_global_queue函式來獲取
  • 全域性佇列是所有應用程式都能夠使用的併發佇列(Concurrent Dispatch Queue),沒必要通過dispatch_queue_create函式逐個生成併發佇列,只需要獲取Global Dispatch Queue即可
  • 是系統為了方便程式設計師開發提供的,其工作表現與併發佇列一致其工作表現與併發佇列一致其工作表現與併發佇列一致
  • Global Dispatch Queue有4個優先順序,分別是高優先順序、預設優先順序、低優先順序、後臺優先順序!因為XNU核心用於Global Dispatch Queue的執行緒並不能保證實時性,因此執行優先順序知識大概的判斷和區分。
全域性佇列同步和非同步執行Demo

全域性佇列 同步執行結果:

全域性佇列 非同步執行結果:

4. 主佇列(Main Dispatch Queue)

主佇列基本知識
  • dispatch_get_main_queue()函式來獲取
  • 專門用來在主執行緒上排程任務的佇列
  • 不會開啟執行緒
  • 以先進先出的方式,在主執行緒空閒時才會排程佇列中的任務在主執行緒執行
  • 如果當前主執行緒正在有任務執行,那麼無論主佇列中當前被新增了什麼任務,都不會被排程
主佇列同步和非同步執行Demo

主佇列 同步執行結果:

主佇列 非同步執行結果:

5.延遲執行(dispatch_after)

  • 在GCD中我們使用dispatch_after()函式來延遲執行佇列中的任務, dispatch_after()是非同步執行佇列中的任務的,也就是說使用dispatch_after()來執行佇列中的任務不會阻塞當前任務。等到延遲時間到了以後就會開闢一個新的執行緒然後執行佇列中的任務。
  • dispatch_after(dispatch_time_t when,dispatch_queue_t queue,dispatch_block_t block);來建立
  • 經過我的猜測測試(看不到原始碼),你們也思考一下這個延遲函式的實現過程。底層實現應該是dispath_async函式追加block到Main Dispatch Queue等相應佇列,虛擬碼如下(以Main Dispatch Queue為例):
延遲執行(dispatch_after)Demo

注意:
  • dispatch_after(dispatch_time_t when,dispatch_queue_t queue,dispatch_block_t block);
  • 第二個引數:指定要追加處理的Dispatch Queue
  • 第三個引數:指定要執行處理的Block
  • 第一個引數:指定時間用的dispatch_time_t型別的值,該值是使用dispatch_time函式或者dispatch_walltime函式生成
    1.dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
    裡面有兩個引數:


    2.使用dispatch_walltime函式生成dispatch_time_t,用於計算絕對時間,例如在dispatch_after函式中指定2016年11月3日21時45分1秒這一絕對時間,可粗略的鬧鐘功能來使用。也就是到指定時間就會執行任務

6. 更改優先順序(dispatch_set_target_queue)

  • dispatch_queue_create函式生成的Dispatch Queue不管是 序列佇列(Serial Dispatch Queue) 還是 併發佇列(Concurrent Dispatch Queue) ,都是用與 預設優先順序全域性佇列(Global Dispatch Queue)相同執行優先順序的執行緒。而全域性佇列工作方式與併發佇列工作方式完全一致!
  • dispatch_set_target_queue(dispatch_object_t object,dispatch_queue_t _Nullable queue);函式來更改佇列優先順序
    • 第一個引數:指定要變更優先順序的佇列(要更改佇列)
    • 第二個引數:指定第一個引數(佇列)要和我們預期佇列執行相同優先順序的佇列(目標佇列)

1. dispatch_set_target_queue第一個Demo:

列印結果:

列印解釋:
  • queue1和queue2設定的目標佇列是全域性佇列(併發),也就是允許要更改的佇列可以開闢多條執行緒(可以超過一條)
  • 程式碼中queue1變更和低優先順序的全域性佇列一樣優先順序的序列佇列,queue2是dispatch_queue_create建立和預設優先順序的全域性佇列一樣優先順序的併發佇列。
  • queue1和queue2都是非同步執行,都會開闢執行緒,queue2是併發佇列所以列印中看到有多個執行緒,並且任務之間也不是順序執行。queue1是序列佇列,所以只會開闢一個執行緒,任務會順序執行。
  • queue2佇列的優先順序比queue1佇列的優先順序要高,從列印中可以看到queue2的確比queue1任務要普遍先執行。為什麼要說普遍呢,因為從結果上看到queue1有一個任務是在queue2裡任務沒有執行完畢也執行了,為什麼會出現這個問題呢??

2. dispatch_set_target_queue第一個Demo:

  • dispatch_set_target_queue除了能用來設定佇列的優先順序之外,還能夠建立佇列的執行層次
    ,當我們想讓不同佇列中的任務同步的執行時,我們可以建立一個序列佇列,然後將這些佇列的target指向新建立的佇列即可,比如:
    13970779-013dbfc9d7364148
    佇列的執行層次.png

列印結果:

列印解釋:
  • queue1和queue2設定的目標佇列是序列佇列,也就是允許要更改的佇列可以開闢一條執行緒。
  • queue1和queue2優先順序一樣,他兩執行順序相當於一個序列佇列非同步執行!
  • queue1和queue2佇列以targetQueue佇列為參照物件,那麼queue1和queue2中的任務將按照targetQueue的佇列處理。
  • 適用場景:一般都是把一個任務放到一個序列的queue中,如果這個任務被拆分了,被放置到多個序列的queue中,但實際還是需要這個任務同步執行,那麼就會有問題,因為多個序列queue之間是並行的。這時候dispatch_set_target_queue將起到作用。

dispatch_set_target_queue小結:

  • dispatch_set_target_queue可以更改Dispatch Queue優先順序。
  • dispatch_set_target_queue可以更改佇列的執行層次,佇列裡的任務將會按照目標佇列(target Queue)的佇列來處理

7. 任務組Dispatch Group

  • GCD的任務組在開發中是經常被使用到,當你一組任務結束後再執行一些操作時,使用任務組在合適不過了。dispatch_group的職責就是當佇列中的所有任務都執行完畢後在去做一些操作,也就是說在任務組中執行的佇列,當佇列中的所有任務都執行完畢後就會發出一個通知來告訴使用者任務組中所執行的佇列中的任務執行完畢了。關於將佇列放到任務組中執行有兩種方式,一種是使用dispatch_group_async()函式,將佇列與任務組進行關聯並自動執行佇列中的任務。另一種方式是手動的將佇列與組進行關聯然後使用非同步將佇列進行執行,也就是dispatch_group_enter()dispatch_group_leave()方法的使用。下方就給出詳細的介紹。
1.佇列與組自動關聯並執行
  • 首先我們來介紹dispatch_group_async()函式的使用方式,該函式會將佇列與相應的任務組進行關聯,並且自動執行。當與任務組關聯的佇列中的任務都執行完畢後,會通過dispatch_group_notify()函式發出通知告訴使用者任務組中的所有任務都執行完畢了。使用通知的方式是不會阻塞當前執行緒的,如果你使用dispatch_group_wait()函式,那麼就會阻塞當前執行緒,直到任務組中的所有任務都執行完畢。
    列印結果:

  • 上面的函式就是使用dispatch_group_async()函式將佇列與任務組進行關聯並執行。首先我們建立了一個全域性佇列(併發),然後又建立了一個型別為dispatch_group_t的任務組group。使用dispatch_group_async()函式將兩者進行關聯並執行。使用dispatch_group_notify()函式進行監聽group中佇列的執行結果,如果執行完畢後,我們就在主執行緒中對結果進行處理。
  • dispatch_group_notify()函式有兩個引數一個是傳送通知的group,另一個是處理返回結果的佇列。
  • dispatch_group_async(dispatch_group_t group,dispatch_queue_t queue,dispatch_block_t block);函式與dispatch_async函式相同,都追加block到指定的Dispatch Queue中,與dispatch_async不同的是指定生成的Dispatch Group為第一個引數。指定的block屬於指定的Dispatch Group,不是Dispatch Queue,切記!
  • 無論向什麼樣的Dispatch Queue中追加處理,使用Dispatch Group都可監視這些處理執行的結束。一旦檢測到所有處理執行結束,就可將結束的處理追加到Dispatch Queue中。這就是使用Dispatch Group的原因。
  • 在追加到Dispatch Group中的處理全部執行結束時,程式碼中使用的dispatch_group_notify函式會將執行的Block追加到Dispatch Queue中,將第一個引數指定為要監視的Dispatch Group。在追加到該Dispatch Group的全部處理執行結束時,將第三個引數的Block追加到第二個引數的Dispatch Queue中。在dispatch_group_notify函式中不管指定什麼樣的Dispatch Queue,屬於Dispatch Group的全部處理在追加指定的Block時都已執行結束。
  • 另外,在Dispatch Group中也可以使用dispatch_group_wait函式僅等待全部處理執行結束。

  • dispatch_group_wait函式的第二個引數指定為等待時間(超時)。它屬於dispatch_time_t型別的值。程式碼中使用的DISPATCH_TIME_FOREVER,意味著永久等待。只要屬於Dispatch Group的操作尚未執行結束,就會一直等待,中途不能取消。

    指定等待時間為1微秒時,應做如下處理:

  • 如果dispatch_group_wait函式的返回值不為0,就意味著雖然經過了指定的時間,但屬於Dispatch Group的某一個處理還在執行中。如果返回值為0,那麼全部處理執行結束。當等待時間為DISPATCH_TIME_FOREVER,由dispatch_group_wait函式返回時,屬於Dispatch Group的處理必定全部執行結束,因此返回值恆為0。
  • 這裡的“等待”是什麼意思?這意味著一旦呼叫dispatch_group_wait函式,該函式就處於呼叫的狀態而不返回。即執行dispatch_group_wait函式的現在的執行緒(當前執行緒)停止。在經過dispatch_group_wait函式中指定的時間或屬於指定Dispatch Group的處理全部執行結束之前,執行該函式的執行緒停止,屬於阻塞狀態。
  • 指定DISPACH_TIME_NOW,則不用任何等待即可判定屬於Dispatch Group的處理是否執行結束。

  • 在主執行緒的RunLoop的每次迴圈中,可檢查執行是否結束,從而不消耗多餘的等待時間,雖然這樣有額可以,但一般在這種情況下,還是推薦用dispatch_group_notify函式追加結束處理到Main Dispatch Queue中。這是因為dispatch_group_notify函式可以簡化程式碼。並且如果你用了diaptach_group_wait等待時間過長,中間不能取消佇列任務這就很坑了!
2. 佇列與組手動關聯並執行
  • 接下來我們將手動的管理任務組與佇列中的關係,也就是不使用dispatch_group_async()函式。我們使用dispatch_group_enter()dispatch_group_leave()函式將佇列中的每次任務加入到到任務組中。首先我們使用dispatch_group_enter()函式進入到任務組中,然後非同步執行佇列中的任務,最後使用dispatch_group_leave()函式離開任務組即可。下面的函式中我們使用了dispatch_group_wait()函式,該函式的職責就是阻塞當前執行緒,來等待任務組中的任務執行完畢。該函式的第一個引數是所要等待的group,第二個引數是等待超時時間,此處我們設定的是DISPATCH_TIME_FOREVER,就說明等待任務組的執行永不超時,直到任務組中所有任務執行完畢。

列印結果:dispatch_group_wait()函式下方的print()函式在所有任務執行完畢之前是不會被呼叫的,因為dispatch_group_wait()會將當前執行緒進行阻塞。當然雖然是手動的將佇列與任務組進行關聯的,感覺display_group_notify()函式還是好用的。

8. 柵欄任務Dispatch_barrier_async

  • barrier顧名思義柵欄、障礙物的意思!
    在訪問資料庫或檔案時,使用Serial Dispatch Queue可避免資料競爭的問題。

    寫入處理確實不可與其他的寫入處理以及包含讀取處理的其他某些處理並行執行。但是如果讀取處理只是與讀取處理並行執行,那麼多個並行執行就不會發生問題。

    也就是說,為了高效率地進行訪問,讀取處理追加到Concurrent Dispatch Queue中,寫入出路在任何一個讀取處理沒有執行的狀態下,追加到Serial Dispatch Queue中即可(在寫入處理結束之前,讀取處理不可執行)。

    雖然利用Dispatch Group和dispatch_set_target_queue函式也可實現,但程式碼會很複雜。有興趣的可以自己嘗試寫寫!
    CD為我們提供了更為聰明的解決辦法——dispatch_barrier_async函式。該函式同dispatch_queue_create函式生成的Concurrent Dispatch Queue一起使用。

列印結果:

  • 使用Concurrent Dispatch Queuedispatch_barrier_async函式可實現高效率的資料庫訪問和檔案訪問,dispatch_barrier_async函式還是比較好理解的。

9. 迴圈執行dispatch_apply

  • dispatch_apply()函式是用來迴圈來執行佇列中的任務的,使用方式為:dispatch_apply(迴圈次數, 任務所在的佇列) { 要迴圈執行的任務 }。使用該函式迴圈執行並行佇列中的任務時,會開闢新的執行緒,不過有可能會在當前執行緒中執行一些任務。而使用dispatch_apply()執行序列佇列中的任務時,會在當前執行緒中執行。無論是使用並行佇列還是序列佇列,dispatch_apply()都會阻塞當前執行函式執行緒。

  • 輸出結果中最後的done必定在最後的位置上,這是因為dispatch_apply函式會等待全部處理執行結束,也就是阻塞當前執行函式執行緒。
  • 另外,由於dispatch_apply函式也與dispatch_sync函式相同,會等待處理執行結束(阻塞),因此推薦在dispatch_async函式中非同步地執行dispatch_apply函式。

列印結果:

10. 佇列的掛起和喚醒

  • 佇列的掛起與喚醒相對較為簡單,如果你想對一個佇列中的任務的執行進行掛起,那麼你就使用dispatch_suspend()函式即可。如果你要喚醒某個掛起的佇列,那麼你就可以使用dispatch_resum()函式。這兩個函式所需的引數都是你要掛起或者喚醒的佇列,鑑於知識點的簡單性就不做過多的贅述了。

列印結果:可以先思考一下列印結果

GCD總結

  1. 開不開執行緒由執行任務的函式決定
    • 非同步開,非同步是多執行緒的代名詞
    • 同步不開
  2. 開幾條執行緒由佇列決定
    • 序列佇列開一條執行緒(GCD會開一條,NSOperation Queue最大併發數為1時也可能開多條)
    • 併發佇列開多條執行緒,具體能開的執行緒數量由底層執行緒池決定
  3. GCD的使用步驟
    • 1.建立block任務
    • 2.建立GCD佇列(序列和併發)
    • 3.把block任務放到GCD佇列裡,以非同步或者同步方式執行GCD佇列

結尾:

能看到這裡的小夥伴都是真愛啊,內容太多了。學完這個你們多思考一下里面方法的聯絡以及實現,好了今天的GCD就講到這裡,中間有什麼錯誤歡迎你們批評指出!下一篇部落格將帶你走進NSOperation。

喜歡的話就請點個喜歡加個關注^_^(樓主好無恥啊)❤️

ThreadDemo下載連結

參考書籍:Objective-C高階程式設計iOS與OS X多執行緒和記憶體管理

相關文章