天天看點

iOS多線程程式設計——淺談GCD

GCD對于iOS開發者來說肯定不陌生,他和NSThread,NSOperation一起作為iOS開發中主要的三種多線程實作方法,而GCD是最最底層的,是以對于作為一個iOSer,GCD是必須掌握的。

我通過對于以下兩篇文章的閱讀,基本上掌握了GCD的基本使用方法。是以首先感謝兩位作者。

  • GCD 深入了解:第一部分
  • iOS多線程開發——GCD的使用與多線程開發淺析(二)

一、基本概念

對于新手來說,最常見同時最容易搞混的的莫過于GCD中的一些基本概念了。

  1. 并行與并發(Parallelism && Concurrency)
    • 并行:顧名思義就是同時行動,兩個任務在兩個線程(Thread)上進行處理,彼此互不幹涉。而究其根本是因為多核進行處理,進而更快的解決任務。(可以類比于兩條水管中同時給遊泳池放水)
    • 并發:顧名思義,同時發生。對于有的性能較差的機子,比如說那些隻有單核的裝置,為了讓使用者感覺能夠同時處理多個任務,他就需要通過不斷的切換正在處理的線程,進而實作一種“僞并行”,這樣就防止使用者因為一個任務進行太久而無法進行下一個任務。(可以類比于我用兩個口給遊泳池放水,不過因為隻有一根水管,而放水的方向有2個方面,是以需要不斷的切換,進而實作兩邊同時達到需要的水位)
    • 區分如下圖:
      iOS多線程程式設計——淺談GCD
  2. 串行與并發(Serial && Concurrent)
    • 串行:由于所有任務在一個線程被執行的時候隻有一個任務會被執行,是以每個任務都要依賴于先來先處理(FIFO)原則,處理所有得到的内容。
    • 并行:當接收到任務的時候,并行狀态下,他會開啟多個線程,将每個任務都配置設定到各個線程中,進而使得每個任務都能夠在同一時間被執行。這樣有效的減少了所消耗的時間。
    • 區分圖如下:
    iOS多線程程式設計——淺談GCD
    iOS多線程程式設計——淺談GCD
  3. 隊列:就像前面所說,當你告訴電腦你需要執行哪些事情的時候,電腦就會把你告訴他的一件件事情放到自己的隊伍中,很明顯,先告訴他的事情放在前面,後告訴他的事情放在後面,即先進先處理(FIFO)原則處理所有事物,就像排隊一樣。
    • 在GCD中主要的隊列有2種,就是前面提到的:
      • 串行隊列
      • 并發隊列
    • 蘋果公司提供給我們的已存在的隊列有5種(不包括後期我們自己添加的内容)
      • 主隊列(main queue):預設情況下所有事情都是在主隊列下進行處理,隻要有編碼經驗的人來說,主隊列很明顯是(串行隊列)。
      • 全局排程隊列(Global Dispatch Queues):預設情況下全局隊列是系統提供的并發隊列的統稱,根據隊列的優先程度不同分為以下幾個(優先度從低到高):
        • background
        • low
        • default
        • high
  4. 同步與異步(synchronization && asynchronous)

    -同步:同步指的是在原來的内容執行完成之後,再執行你下面所需要的任務。

    -異步:異步指的是在和原來的内容執行的同時,在另一個地方同時處理新的任務。

二、基本使用

既然學了,那肯定要使用它。那麼我們先模拟一個環境:

這個程式裡面隻有主隊列,然後你想從網上下載下傳一個圖檔,放到你自己的手機裡面,然後使用者就可以對手機裡面的這張圖檔進行各種各樣的處理。

但是問題來了:我們都知道下載下傳需要時間,雖然現在已經是4G時代,下載下傳速度很快,但是如果這個圖檔很大,那麼他就需要很長的時間來進行等待。而在這段時間内,使用者什麼事情都不能幹,隻能默默的幹等着。

這很明顯是一個很差的使用者體驗

那麼既然我們已經知道問題所在,就需要想辦法解決它:

  1. 我們需要在使用者等待的時候添加一個動畫效果,告訴他我們正在下載下傳(這個和今天主題無關,我們就不繼續深入了解)
  2. 将下載下傳任務放到背景(如果仍舊放在前台的話,就算有動畫,動畫效果也不會動)

那麼這個時候就需要異步隊列的存在了

就像前面的基本概念中所提到的,執行分為同步執行和異步執行,任務隊列分為串行隊列和并發隊列。是以他們彼此一一結合,總共有4種情況存在,主要的彼此結合的狀态圖如下所示:

iOS多線程程式設計——淺談GCD

單隊列

串行隊列,同步執行

代碼如下:

func serialDispatchQueueWithSync() {
    let queue = dispatch_queue_create("serialDispatchQueue", DISPATCH_QUEUE_SERIAL)

    print("0")

    dispatch_sync(queue) {
        print("1")
    }
    dispatch_sync(queue) {
        print("2")
    }
    dispatch_sync(queue) {
        print("3")
    }
    dispatch_sync(queue) {
        print("4")
    }
    dispatch_sync(queue) {
        print("5")
    }
    print("6")
}           

運作結果如下:

0
1
2
3
4
5
6           

串行隊列,異步執行

代碼如下:

func serialDispatchQueueWithASync() {
    let queue = dispatch_queue_create("serial", DISPATCH_QUEUE_SERIAL)

    print("0")

    dispatch_async(queue) {
        print("1")
    }
    dispatch_async(queue) {
        print("2")
    }
    dispatch_async(queue) {
        print("3")
    }
    dispatch_async(queue) {
        print("4")
    }
    dispatch_async(queue) {
        print("5")
    }
    dispatch_async(queue) {
        print("6")
    }
    print("7")
}           

運作結果如下:

0
7
1
2
3
4
5
6           

解釋:因為queue是異步執行的,即調用dispatch_async函數,是以輸出1 2 3 4 5 6的時候是在另一個線程中的,是以和主線程中的輸出1 7 無關。

并發隊列,同步執行

代碼如下:

func concurrentDispatchsQueueWithSync() {
    let queue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT)

    print("0")

    dispatch_sync(queue) {
        print("1")
        print("12")
        print("13")
    }
    dispatch_sync(queue) {
        print("2")
        print("22")
        print("23")
    }
    dispatch_sync(queue) {
        print("3")
        print("32")
        print("33")
    }
    dispatch_sync(queue) {
        print("4")
        print("42")
        print("43")
    }
    print("5")
}           

運作結果如下:

0
1
12
13
2
22
23
3
32
33
4
42
43
5           

并發隊列,異步執行

代碼如下:

func concurrentDispatchsQueueWithASync() {
    let queue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT)

    print("0")

    dispatch_async(queue) {
        print("1")
        print("12")
        print("13")
    }
    dispatch_async(queue) {
        print("2")
        print("22")
        print("23")
    }
    dispatch_async(queue) {
        print("3")
        print("32")
        print("33")
    }
    dispatch_async(queue) {
        print("4")
        print("42")
        print("43")
    }
    print("5")
}           

運作結果如下:

0
7
1
2
12
3
22
13
4
32
23
5
42
33
52
43
53           

其他常見用法:

挂起與恢複
  • 線程挂起:

    dispatch_suspnd()

  • 線程恢複:

    dispatch_resume()

信号量

由于有的時候(比如說在建立添加數組中的對象的時候),因為不同線程操縱的是同一個對象,是以很容易發生報錯,這個時候需要通過信号量來對對應的内容進行控制,當信号量為0的時候,進入等待狀态,不能執行下面的内容;當信号量為1的時候,可以執行,同時信号量減一,等到執行完畢,信号量加一。

- 等待執行

dispatch_semaphore_wait()

- 信号量加一

dispatch_semahore_signal

隻執行一次

有的時候,有的東西的建立隻能被建立一次(即單例),這個時候就需要用到

dispatch_once()

代碼如下:

var token: dispatch_once_t = 0
func test() {
    dispatch_once(&token) {
        println("This is printed only on the first call to test()")
    }
    println("This is printed for each call to test()")
}           

關于讀寫問題的解決方法(買票問題)

由于日常編碼的過程中經常會遇到,當你在調用這個這個變量的時候。另一個線程也在調用這個變量(隻在并發過程中),如果兩個線程都是在讀取資料,那并沒有什麼問題,但是如果其中一個在寫入,或者兩個都在寫入,那麼就會出現很大問題,是以蘋果官方在GCD中也給我們準備了一個方法,讓我們解決這個問題,那就是

dispatch_barrier_async

,這個方法讓對的内容在并發過程中加入,進而友善組織内容的修改,進而使得對應的内容隻能在目前線程中被進行修改。

iOS多線程程式設計——淺談GCD

常見問題:

死鎖:
let queue = dispatch_get_main_queue()

dispatch_async(queue) { 
    dispatch_sync(queue, { 
        print("1")
    })
}           

産生原理:因為

dispatch_sync()

會等到本身結束之後才會在主線程繼續執行接下去的代碼,但是

dispatch_sync()

這個方法調用的就是主線程,是以午飯等到主線程結束,是以就無法傳回,就會卡在這裡。

解釋:由于是并發隊列,是以他會建立多個線程,進而保證每個線程的任務都能夠盡快完成,是以順序有一定的出入。

最後通過兩張動态圖來最後總結單隊列:

dispatch_sync
- (void)viewDidLoad
{
  [super viewDidLoad];

  dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{

    NSLog(@"First Log");

  });

  NSLog(@"Second Log");
}           
iOS多線程程式設計——淺談GCD
  1. 主隊列按照預定的順序下來
  2. viewDidLoad在主線程進行執行。
  3. 直到執行到

    dispatch_sync

  4. 調用

    dispatch_sync

    代碼,将block添加到全局隊列中,主隊列挂起。
  5. 全局隊列先完成之前存放在全局隊列中的内容。
  6. 完成之前的任務後,執行

    dispatch_sync

    的block中的内容。
  7. 完成block中的任務,主隊列上的任務得以恢複
  8. 主隊列繼續執行其他任務。
dispatch_async
- (void)viewDidLoad
{
  [super viewDidLoad];

  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{

    NSLog(@"First Log");

  });

  NSLog(@"Second Log");
}

![dispatch_sync](https://camo.githubusercontent.com/2c7cbaf76001a56622e14cf48a8d914d4b5c9df4/687474703a2f2f63646e312e72617977656e6465726c6963682e636f6d2f77702d636f6e74656e742f75706c6f6164732f323031342f30312f64697370617463685f6173796e635f696e5f616374696f6e2e676966)
           
  1. 主隊列一路按順序執行任務——接着是一個執行個體化

    UIViewController

    的任務,其中包含了

    viewDidLoad

  2. viewDidLoad

    在主線程執行。
  3. 主線程目前在

    viewDidLoad

    内,正要到達

    dispatch_async

  4. dispatch_async Block

    被添加到一個全局隊列中,将在稍後執行。
  5. viewDidLoad

    在添加

    dispatch_async

    到全局隊列後繼續進行,主線程把注意力轉向剩下的任務。同時,全局隊列并發地處理它未完成地任務。記住

    Block

    在全局隊列中将按照(FIFO)順序出列,但可以并發執行。
  6. 添加到

    dispatch_async

    的代碼塊開始執行。
  7. dispatch_async Block

    完成,兩個

    NSLog

    語句将它們的輸出放在控制台上。

排程組

既然能夠處理一個個的任務,那麼我們就繼續模拟一個環境,當我們需要在網上下載下傳内容的時候(這些内容需要彼此聯系在一起才能正常使用),這個時候,上面的單隊列就不夠了(或者說如果使用單隊列産生的效果不是時間太長,就是檔案的完整性不夠好)

這個時候我們就需要引入多隊列,當多個内容都處理完成之後,讓系統告訴我們,我們已經完成了以上的下載下傳。可以繼續做下一步事情了。

任務開始

在GCD中,我們可以通過

dispatch_group_enter

來通知目前任務的開始,而與之相對應的,我們必須在任務完成後,手動通知排程組任務結束(

dispatch_group_leave

)這樣才能讓排程組知道我們這個任務已經結束。

代碼如下(由于demo中有一個Photo類,是以此處貼上OC代碼):

dispatch_group_enter(downloadGroup); // 3
Photo *photo = [[Photo alloc] initwithURL:url withCompletionBlock:^(UIImage *image, NSError *_error) {
    if (_error) {
        error = _error;
    }
dispatch_group_leave(downloadGroup); // 4
}];

[[PhotoManager sharedManager] addPhoto:photo];
           

任務提醒

當我們所有的任務都手動通知後,那麼就需要條用提醒來告訴他(我已經完成了所有内容,接下去需要試試哪裡能夠執行了),而提醒方式在GCD中有兩種:

  • dispatch_group_wait() + dispatch_async()

  • dispatch_group_notify

其他常見用法

dispatch_apply()

有的時候需要調用for循環來反複執行,但是當需要執行的代碼量偏大的時候,for的效率比較低,這個時候需要用

dispatch_apply()

來執行,這樣節省效率,不過當需要執行的代碼量比較小的時候,

dispatch_apply()

的效率就比較差了。

這個方法的效果和

dispatch_sync

一樣,是以要注意死鎖(後面會提到)