GCD(一) 隊列、任務、串行、併發

本文是GCD多線程編程基礎內容的小結,經過本文,你能夠了解到:前端

  • 多線程的幾個基本概念:進程與線程串行與併發
  • GCD中的2個核心內容:隊列任務
  • GCD的基本使用步驟
  • GCD中使用同步異步方式添加任務到串行併發隊列後執行的實際效果
  • GCD中產生死鎖的緣由以及實際開發中如何避免死鎖crash

GCD

Apple爲了讓開發者更加容易的使用設備上的多核CPU,蘋果在 OS X 10.6 和 iOS 4 中引入了 Grand Central Dispatch(GCD),它是 Apple 開發的一個多核編程的較新的解決方法,它主要用於優化應用程序以支持多核處理器,它是一個在線程池模式的基礎上執行的併發任務,是咱們日常開發中最多見的一種多線程編程方式ios

測試代碼在這git

多線程基本概念

進程與線程

對於操做系統來講,一個任務就是一個進程(Process),好比打開一個瀏覽器就是啓動一個瀏覽器進程,打開一個記事本就啓動了一個記事本進程,打開兩個記事本就啓動了兩個記事本進程,打開一個Word就啓動了一個Word進程。github

有些進程還不止同時幹一件事,好比Word,它能夠同時進行打字、拼寫檢查、打印等事情。在一個進程內部,要同時幹多件事,就須要同時運行多個「子任務」,咱們把進程內的這些「子任務」稱爲線程(Thread)。數據庫

因爲每一個進程至少要幹一件事,因此,一個進程至少有一個線程。固然,像Word這種複雜的進程能夠有多個線程,多個線程能夠同時執行,多線程的執行方式和多進程是同樣的,也是由操做系統在多個線程之間快速切換,讓每一個線程都短暫地交替運行,看起來就像同時執行同樣。固然,真正地同時執行多線程須要多核CPU纔可能實現。macos

串行和併發

併發就是多個任務在執行的過程當中,時間互相重疊,一個任務執行沒結束,另外一個已經開始。編程

串行就是任務一個一個的執行,時間上不相互重疊,一個任務執行結束,下一個任務才能開始執行。後端

GCG隊列

隊列是一種特殊的線性表,特殊之處在於它只容許在表的後端進入插入操做,在表的前端進行刪除操做,即遵循FIFO原則。瀏覽器

GCD中的隊列(Dispatch Queue)就是指用來執行任務的等待隊列,當咱們添加任務到隊列以後,開發者不用再直接跟線程打交道了,只須要向隊列中添加代碼塊便可,GCD 在後端管理着一個線程池。GCD 不只決定着你的代碼塊將在哪一個線程被執行,它還根據可用的系統資源對這些線程進行管理。這樣能夠將開發者從線程管理的工做中解放出來,經過集中的管理線程,來緩解大量線程被建立的問題。安全

GCD中的隊列能夠分爲如下2種:

  • 串行隊列 ( Serial Dispatch Queue )

    串行隊列(也稱爲私有調度隊列)按照將他們添加到隊列順序一次執行一個任務。當前正在執行的任務在由隊列管理的不一樣線程(可能因任務而異)上運行。串行隊列一般用於同步對特定資源的訪問。

  • 併發隊列 ( Concurrent Dispatch Queue )

    併發隊列(也稱爲一種全局調度隊列)同時執行一個或多個任務,但任務仍按其添加到隊列的順序啓動。當前正在執行的任務在由調度隊列管理的不一樣線程上運行。在任何給定點執行的任務的確切數量是可變的,取決於系統條件。

在咱們平時的開發中,還有2種咱們最多見的,也是使用頻率最高的隊列:

  • 主隊列 ( Main Dispatch Queue )

    主隊列是一個全局可用的串行隊列,它在應用程序的主線程上執行任務。此隊列與應用程序的Runloop一塊兒工做,將有序任務的執行與附加到Runloop的其餘事件源的執行交錯。由於它在應用程序的主線程上運行,因此主隊列一般用做應用程序的關鍵同步點。

    主隊列下的任務無論是異步任務仍是同步任務都不會開闢線程,任務只會在主線程順序執行

  • 全局併發隊列 ( Global Dispatch Queue )

    全局併發隊列本質上是一個併發隊列,有系統提供,方便編程,能夠不用建立就能夠直接使用

GCD任務

任務就是你要在線程中執行的代碼,在GCD中是用Block來定義任務的,是用起來很是靈活便捷。

GCD中執行任務的方式有兩種:同步執行(sync)與異步執行(async)

  • 同步執行

    同步執行就是指使用 dispatch_sync方法將任務同步的添加到隊列裏,在添加的任務執行結束以前,當前線程會被阻塞,而後會一直等待,直到任務完成。

    dispatch_sync添加的任務只能在當前線程執行,不具有開啓新線程的能力

  • 異步執行

    異步執行就是指使用dispatch_async方法將任務異步的添加到隊列裏,它不須要等待任務執行結束,不須要作任何等待就能繼續執行任務

    dispatch_async添加的任務能夠在新的線程中執行任務,具有開啓新線程的能力,但並不必定會開啓新線程

GCD的使用步驟

這個就跟趙本山跟宋丹丹的小品《鐘點工》裏提出的把大象裝進冰箱的經典問題同樣,都是分三步:

把大象裝進冰箱

  1. 把冰箱門打開
  2. 把大象裝進去
  3. 把冰箱門關上

GCD使用步驟

  1. 建立或獲取一個隊列
  2. 定製須要執行的任務
  3. 將任務追加到隊列

建立或獲取一個隊列

  • 使用dispatch_get_main_queue() 獲取主隊列。

  • 使用dispatch_get_global_queue獲取全局併發隊列,這個函數有2個參數,第一個參數是全局隊列的優先級,通常狀況下,使用的都是DISPATCH_QUEUE_PRIORITY_DEFAULT優先級,第二個參數是一個保留字段,咱們須要給它一個0,不然這個函數會返回一個NULL,致使咱們獲取不到正常的全局隊列。

    Reserved for future use. Passing any value other than zero may result in a NULL return value.
    複製代碼
  • 使用dispatch_queue_create函數,建立自定義的串行或並行隊列,這個函數的定義以下:

    * @param label
     * A string label to attach to the queue.
     * This parameter is optional and may be NULL.
     *
     * @param attr
     * A predefined attribute such as DISPATCH_QUEUE_SERIAL,
     * DISPATCH_QUEUE_CONCURRENT, or the result of a call to
     * a dispatch_queue_attr_make_with_* function.
     *
     * @result
     * The newly created dispatch queue.
     */
    API_AVAILABLE(macos(10.6), ios(4.0))
    DISPATCH_EXPORT DISPATCH_MALLOC DISPATCH_RETURNS_RETAINED DISPATCH_WARN_RESULT
    DISPATCH_NOTHROW
    dispatch_queue_t
    dispatch_queue_create(const char *_Nullable label,
    		dispatch_queue_attr_t _Nullable attr);
    複製代碼

    dispatch_queue_create函數最後返回了一個隊列dispatch_queue_t,這個函數有2個參數,第一個參數其實能夠當作是是咱們給這個隊列取的名字,以便後續debug,蘋果官方是推薦開發者使用逆序全程域名。

    第二個參數,是用於肯定這個隊列是串行隊列仍是併發隊列,使用DISPATCH_QUEUE_CONCURRENT表示建立的隊列是併發隊列,使用DISPATCH_QUEUE_SERIAL或者NULL表示建立的隊列是串行隊列,它們兩個實際上是等價的,見下面的註釋:

    /*! * @const DISPATCH_QUEUE_SERIAL * * @discussion * An attribute that can be used to create a dispatch queue that invokes blocks * serially in FIFO order. * * See dispatch_queue_serial_t. */
    #define DISPATCH_QUEUE_SERIAL NULL
    複製代碼

    不過我的不推薦使用NULL的方式來表示建立的是串行隊列,這種方式在多人開發時,閱讀性是比較差的。

    如下是獲取或建立的4種方式:

    //獲取自定義串行隊列
        self.serialQueue = dispatch_queue_create("com.zed.customSerialQueue", DISPATCH_QUEUE_SERIAL);
        //獲取自定義併發隊列
        self.concurrentQueue = dispatch_queue_create("com.zed.customConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
        //獲取主隊列
        dispatch_queue_t mainQueue = dispatch_get_main_queue();
        //獲取全局併發隊列
        dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    複製代碼

定製須要執行的任務

GCD種的任務其實就是一個Block,就是咱們俗稱的代碼塊,在這個代碼塊裏面,把咱們須要作的事情就是,將咱們的任務代碼加入到這個block中

void (^block)(void) = ^{
        NSLog(@"執行任務");
        for (int i = 0; i<100; i++) {
            NSLog(@"%d",i);
        }
        NSLog(@"Thread:%@",[NSThread currentThread]);
    };
複製代碼

將任務追加到隊列

GCD提供了2個方法用於將任務追加到隊列:

  1. dispatch_sync 使用同步執行的方式追加到隊列
  2. dispatch_async 使用異步的方式追加到隊列
//3、將任務增長到隊列中
dispatch_async(globalQueue, block);
複製代碼

GCD的基本使用

前面咱們已經介紹了兩種基本隊列(串行隊列與併發隊列),兩種特殊隊列(主隊列與全局併發隊列),兩種任務執行方式(同步執行與異步執行),因此,咱們就有了8中不一樣的組合方式,不過因爲全局併發隊列跟普通併發隊列的性質是差很少的,因此,咱們就有6中不一樣的組合,接下來,咱們從3個角度來觀察這6種組合方式的效果:

三個角度

  1. 是否開啓線程
  2. 任務是按序執行仍是交替(同時)執行
  3. 是否阻塞當前線程

六種組合方式

  1. 同步執行+併發隊列
  2. 異步執行+併發隊列
  3. 同步執行+串行隊列
  4. 異步執行+串行隊列
  5. 同步執行+主隊列
  6. 異步執行+主隊列

同步執行+併發隊列

#pragma mark - 同步執行+併發隊列
/* * 特色: * 1.在當前線程中執行任務,不會開啓新線程 * 2.按序執行任務,執行行完一個任務,再執行下一個任務 * 3.會阻塞當前線程 */
- (IBAction)executeSyncConcurrencyTask:(UIButton *)sender {
    
    NSLog(@"CurrentThread---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"SyncConcurrencyTask---begin");
    
    dispatch_sync(self.concurrentQueue, ^{
        // 追加任務1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模擬耗時操做
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印當前線程
        }
    });
    
    dispatch_sync(self.concurrentQueue, ^{
        // 追加任務2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模擬耗時操做
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印當前線程
        }
    });
    
    dispatch_sync(self.concurrentQueue, ^{
        // 追加任務3
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模擬耗時操做
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印當前線程
        }
    });
    
    NSLog(@"SyncConcurrencyTask---end");
    NSLog(@"*********************************************************");
}

複製代碼

執行結果以下:

2019-04-22 13:42:07.265811+0800 GCD(一) 隊列、任務、串行、併發[8668:1868026] CurrentThread---<NSThread: 0x600002969400>{number = 1, name = main}
2019-04-22 13:42:07.265945+0800 GCD(一) 隊列、任務、串行、併發[8668:1868026] SyncConcurrencyTask---begin
2019-04-22 13:42:09.266681+0800 GCD(一) 隊列、任務、串行、併發[8668:1868026] 1---<NSThread: 0x600002969400>{number = 1, name = main}
2019-04-22 13:42:11.268049+0800 GCD(一) 隊列、任務、串行、併發[8668:1868026] 1---<NSThread: 0x600002969400>{number = 1, name = main}
2019-04-22 13:42:12.299360+0800 GCD(一) 隊列、任務、串行、併發[8668:1868133] XPC connection interrupted
2019-04-22 13:42:13.269544+0800 GCD(一) 隊列、任務、串行、併發[8668:1868026] 2---<NSThread: 0x600002969400>{number = 1, name = main}
2019-04-22 13:42:15.270540+0800 GCD(一) 隊列、任務、串行、併發[8668:1868026] 2---<NSThread: 0x600002969400>{number = 1, name = main}
2019-04-22 13:42:17.271936+0800 GCD(一) 隊列、任務、串行、併發[8668:1868026] 3---<NSThread: 0x600002969400>{number = 1, name = main}
2019-04-22 13:42:19.273478+0800 GCD(一) 隊列、任務、串行、併發[8668:1868026] 3---<NSThread: 0x600002969400>{number = 1, name = main}
2019-04-22 13:42:19.273713+0800 GCD(一) 隊列、任務、串行、併發[8668:1868026] SyncConcurrencyTask---end
2019-04-22 13:42:19.273857+0800 GCD(一) 隊列、任務、串行、併發[8668:1868026] *********************************************************

複製代碼

經過咱們的代碼測驗,能夠看出:

  1. 全部的任務都是在主線程(當前線程)中執行的,並無開啓新的線程,這也說明了同步執行的一個特性:同步執行任務不具有開啓新線程的能力。

  2. 任務一、任務二、任務3是按順序執行的,並無出現併發執行的狀況,這是由於雖然併發隊列具有同時執行多個任務的能力,可是因爲是同步執行不具有開啓新線程的能力,因此,即便任務被追加到了併發隊列,它也沒有辦法去開啓新的線程,只能在當前線程中執行任務。

  3. 從咱們的log中能夠看出,咱們全部的任務都是在 beginend之間的,因此說,它會阻塞當前線程,等待隊列中的任務執行結束,纔會繼續執行下面的代碼。

異步執行+併發隊列

#pragma mark - 異步執行+併發隊列
/* * 特色: * 1.開啓多個新線程執行任務 * 2.任務交替(同時)執行 * 3.不會阻塞當前線程 */
- (IBAction)executeAsyncConcurrencyTask:(UIButton *)sender {
    
    NSLog(@"CurrentThread begin---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"AsyncConcurrencyTask---begin");
    
    dispatch_async(self.concurrentQueue, ^{
        // 追加任務1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模擬耗時操做
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印當前線程
        }
    });
    
    dispatch_async(self.concurrentQueue, ^{
        // 追加任務2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模擬耗時操做
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印當前線程
        }
    });
    
    dispatch_async(self.concurrentQueue, ^{
        // 追加任務3
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模擬耗時操做
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印當前線程
        }
    });
    
    NSLog(@"CurrentThread end---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"AsyncConcurrencyTask---end");
    NSLog(@"*********************************************************");
}
複製代碼

執行結果以下:

2019-04-22 14:32:14.941222+0800 GCD(一) 隊列、任務、串行、併發[9417:2009244] CurrentThread begin---<NSThread: 0x6000000cea40>{number = 1, name = main}
2019-04-22 14:32:14.941405+0800 GCD(一) 隊列、任務、串行、併發[9417:2009244] AsyncConcurrencyTask---begin
2019-04-22 14:32:14.941608+0800 GCD(一) 隊列、任務、串行、併發[9417:2009244] CurrentThread end---<NSThread: 0x6000000cea40>{number = 1, name = main}
2019-04-22 14:32:14.941757+0800 GCD(一) 隊列、任務、串行、併發[9417:2009244] AsyncConcurrencyTask---end
2019-04-22 14:32:14.941894+0800 GCD(一) 隊列、任務、串行、併發[9417:2009244] *********************************************************
2019-04-22 14:32:16.945860+0800 GCD(一) 隊列、任務、串行、併發[9417:2009294] 1---<NSThread: 0x6000000a2380>{number = 4, name = (null)}
2019-04-22 14:32:16.945909+0800 GCD(一) 隊列、任務、串行、併發[9417:2011461] 3---<NSThread: 0x6000000a2340>{number = 6, name = (null)}
2019-04-22 14:32:16.945909+0800 GCD(一) 隊列、任務、串行、併發[9417:2011460] 2---<NSThread: 0x6000000aec00>{number = 5, name = (null)}
2019-04-22 14:32:18.951120+0800 GCD(一) 隊列、任務、串行、併發[9417:2011460] 2---<NSThread: 0x6000000aec00>{number = 5, name = (null)}
2019-04-22 14:32:18.951121+0800 GCD(一) 隊列、任務、串行、併發[9417:2011461] 3---<NSThread: 0x6000000a2340>{number = 6, name = (null)}
2019-04-22 14:32:18.951120+0800 GCD(一) 隊列、任務、串行、併發[9417:2009294] 1---<NSThread: 0x6000000a2380>{number = 4, name = (null)}

複製代碼

經過咱們的代碼測驗,能夠看出:

  1. 除了在主線程執行的2個log任務以外,系統又開啓了3個線程用於執行追加的三個任務,說明異步執行具有開啓新線程的能力,而且併發隊列能夠開啓多個線程,交替執行多個任務。
  2. 從咱們的log中能夠看到,begin的log以後,立刻就是end的log,所以能夠看出,它並不會阻塞當前線程,並不須要等待追加的任務執行完成。

同步執行+串行隊列

#pragma mark - 同步執行+串行隊列
/* * 特色: * 1.在當前線程中執行任務,不會開啓新線程 * 2.按序執行任務,執行行完一個任務,再執行下一個任務 * 3.會阻塞當前線程 */
- (IBAction)executeSyncSerialTask:(UIButton *)sender {
    
    NSLog(@"CurrentThread begin---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"SyncSerialTask---begin");
    
    dispatch_sync(self.serialQueue, ^{
        // 追加任務1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模擬耗時操做
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印當前線程
        }
    });
    
    dispatch_sync(self.serialQueue, ^{
        // 追加任務2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模擬耗時操做
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印當前線程
        }
    });
    
    dispatch_sync(self.serialQueue, ^{
        // 追加任務3
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模擬耗時操做
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印當前線程
        }
    });
    
    NSLog(@"CurrentThread end---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"SyncSerialTask---end");
    NSLog(@"*********************************************************");
}
複製代碼

執行結果以下:

2019-04-22 15:02:52.760352+0800 GCD(一) 隊列、任務、串行、併發[9826:2087150] CurrentThread begin---<NSThread: 0x600000222800>{number = 1, name = main}
2019-04-22 15:02:52.760558+0800 GCD(一) 隊列、任務、串行、併發[9826:2087150] SyncSerialTask---begin
2019-04-22 15:02:54.761971+0800 GCD(一) 隊列、任務、串行、併發[9826:2087150] 1---<NSThread: 0x600000222800>{number = 1, name = main}
2019-04-22 15:02:56.762653+0800 GCD(一) 隊列、任務、串行、併發[9826:2087150] 1---<NSThread: 0x600000222800>{number = 1, name = main}
2019-04-22 15:02:58.764202+0800 GCD(一) 隊列、任務、串行、併發[9826:2087150] 2---<NSThread: 0x600000222800>{number = 1, name = main}
2019-04-22 15:03:00.765234+0800 GCD(一) 隊列、任務、串行、併發[9826:2087150] 2---<NSThread: 0x600000222800>{number = 1, name = main}
2019-04-22 15:03:02.766464+0800 GCD(一) 隊列、任務、串行、併發[9826:2087150] 3---<NSThread: 0x600000222800>{number = 1, name = main}
2019-04-22 15:03:04.767966+0800 GCD(一) 隊列、任務、串行、併發[9826:2087150] 3---<NSThread: 0x600000222800>{number = 1, name = main}
2019-04-22 15:03:04.768230+0800 GCD(一) 隊列、任務、串行、併發[9826:2087150] CurrentThread end---<NSThread: 0x600000222800>{number = 1, name = main}
2019-04-22 15:03:04.768379+0800 GCD(一) 隊列、任務、串行、併發[9826:2087150] SyncSerialTask---end
2019-04-22 15:03:04.768516+0800 GCD(一) 隊列、任務、串行、併發[9826:2087150] *********************************************************
複製代碼

經過咱們的代碼測驗,能夠看出:

  1. 全部的任務都是在主線程(當前線程)中執行的,而且是順序執行的,沒有開啓新的線程。
  2. 從咱們的log中能夠看出,咱們全部的任務都是在 beginend之間的,因此說,它會阻塞當前線程,等待隊列中的任務執行結束,纔會繼續執行下面的代碼。

異步執行+串行隊列

#pragma mark - 異步執行+串行隊列
/* * 特色: * 1.會開啓一條新線程 * 2.按序執行任務,執行行完一個任務,再執行下一個任務 * 3.不會阻塞當前線程 */
- (IBAction)executeAsyncSerialTask:(UIButton *)sender {
    
    NSLog(@"CurrentThread begin---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"AsyncSerialTask---begin");
    
    dispatch_async(self.serialQueue, ^{
        // 追加任務1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模擬耗時操做
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印當前線程
        }
    });
    
    dispatch_async(self.serialQueue, ^{
        // 追加任務2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模擬耗時操做
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印當前線程
        }
    });
    
    dispatch_async(self.serialQueue, ^{
        // 追加任務3
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模擬耗時操做
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印當前線程
        }
    });
    
    NSLog(@"CurrentThread end---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"AsyncSerialTask---end");
    NSLog(@"*********************************************************");
}
複製代碼

執行結果以下:

2019-04-22 15:25:00.103488+0800 GCD(一) 隊列、任務、串行、併發[10181:2154024] CurrentThread begin---<NSThread: 0x6000019c9400>{number = 1, name = main}
2019-04-22 15:25:00.103734+0800 GCD(一) 隊列、任務、串行、併發[10181:2154024] AsyncSerialTask---begin
2019-04-22 15:25:00.103888+0800 GCD(一) 隊列、任務、串行、併發[10181:2154024] CurrentThread end---<NSThread: 0x6000019c9400>{number = 1, name = main}
2019-04-22 15:25:00.103986+0800 GCD(一) 隊列、任務、串行、併發[10181:2154024] AsyncSerialTask---end
2019-04-22 15:25:00.104091+0800 GCD(一) 隊列、任務、串行、併發[10181:2154024] *********************************************************
2019-04-22 15:25:02.108899+0800 GCD(一) 隊列、任務、串行、併發[10181:2154074] 1---<NSThread: 0x600001992fc0>{number = 4, name = (null)}
2019-04-22 15:25:04.111910+0800 GCD(一) 隊列、任務、串行、併發[10181:2154074] 1---<NSThread: 0x600001992fc0>{number = 4, name = (null)}
2019-04-22 15:25:06.116733+0800 GCD(一) 隊列、任務、串行、併發[10181:2154074] 2---<NSThread: 0x600001992fc0>{number = 4, name = (null)}
2019-04-22 15:25:08.117706+0800 GCD(一) 隊列、任務、串行、併發[10181:2154074] 2---<NSThread: 0x600001992fc0>{number = 4, name = (null)}
2019-04-22 15:25:10.122737+0800 GCD(一) 隊列、任務、串行、併發[10181:2154074] 3---<NSThread: 0x600001992fc0>{number = 4, name = (null)}
2019-04-22 15:25:12.126742+0800 GCD(一) 隊列、任務、串行、併發[10181:2154074] 3---<NSThread: 0x600001992fc0>{number = 4, name = (null)}

複製代碼

經過咱們的代碼測驗,能夠看出:

  1. 三個追加的任務都是在一個新的線程中執行的,在串行隊列中異步執行任務,會開啓一條新線程,因爲隊列是串行的,因此任務是按序執行的。
  2. 從咱們的log中能夠看到,begin的log以後,立刻就是end的log,所以能夠看出,它並不會阻塞當前線程,並不須要等待追加的任務執行完成。

異步執行+主隊列

#pragma mark - 異步執行+主隊列
/* * 特色: * 1.在當前線程(主線程)中執行任務 * 2.按序執行任務,執行行完一個任務,再執行下一個任務 * 3.不會阻塞當前線程 */
- (IBAction)executeAsyncMainQueueTask:(UIButton *)sender {
    
    NSLog(@"CurrentThread begin---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"AsyncMainQueueTask---begin");
    
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
    dispatch_async(mainQueue, ^{
        // 追加任務1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模擬耗時操做
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印當前線程
        }
    });
    
    dispatch_async(mainQueue, ^{
        // 追加任務2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模擬耗時操做
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印當前線程
        }
    });
    
    dispatch_async(mainQueue, ^{
        // 追加任務3
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模擬耗時操做
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印當前線程
        }
    });
    
    NSLog(@"CurrentThread end---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"AsyncMainQueueTask---end");
    NSLog(@"*********************************************************");
}

複製代碼

執行結果以下:

2019-04-22 18:28:03.990381+0800 GCD(一) 隊列、任務、串行、併發[12894:2615623] CurrentThread begin---<NSThread: 0x600003ee6900>{number = 1, name = main}
2019-04-22 18:28:03.990589+0800 GCD(一) 隊列、任務、串行、併發[12894:2615623] AsyncMainQueueTask---begin
2019-04-22 18:28:03.990808+0800 GCD(一) 隊列、任務、串行、併發[12894:2615623] CurrentThread end---<NSThread: 0x600003ee6900>{number = 1, name = main}
2019-04-22 18:28:03.990959+0800 GCD(一) 隊列、任務、串行、併發[12894:2615623] AsyncMainQueueTask---end
2019-04-22 18:28:03.991088+0800 GCD(一) 隊列、任務、串行、併發[12894:2615623] *********************************************************
2019-04-22 18:28:05.993620+0800 GCD(一) 隊列、任務、串行、併發[12894:2615623] 1---<NSThread: 0x600003ee6900>{number = 1, name = main}
2019-04-22 18:28:07.994036+0800 GCD(一) 隊列、任務、串行、併發[12894:2615623] 1---<NSThread: 0x600003ee6900>{number = 1, name = main}
2019-04-22 18:28:09.995547+0800 GCD(一) 隊列、任務、串行、併發[12894:2615623] 2---<NSThread: 0x600003ee6900>{number = 1, name = main}
2019-04-22 18:28:11.997136+0800 GCD(一) 隊列、任務、串行、併發[12894:2615623] 2---<NSThread: 0x600003ee6900>{number = 1, name = main}
2019-04-22 18:28:13.997717+0800 GCD(一) 隊列、任務、串行、併發[12894:2615623] 3---<NSThread: 0x600003ee6900>{number = 1, name = main}
2019-04-22 18:28:15.998158+0800 GCD(一) 隊列、任務、串行、併發[12894:2615623] 3---<NSThread: 0x600003ee6900>{number = 1, name = main}

複製代碼

經過咱們的代碼測驗,能夠看出:

  1. 3個任務在主線程中,按序執行

  2. 從咱們的log中能夠看到,begin的log以後,立刻就是end的log,所以能夠看出,它並不會阻塞當前線程,並不須要等待追加的任務執行完成。

同步執行+主隊列

#pragma mark - 同步執行+主隊列
/* * 特色: * 會直接產生死鎖 */
- (IBAction)executeSyncMainQueueTask:(UIButton *)sender {
    
    NSLog(@"CurrentThread begin---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"SyncMainQueueTask---begin");
    
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
    dispatch_sync(mainQueue, ^{
        // 追加任務1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模擬耗時操做
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印當前線程
        }
    });
    
    dispatch_sync(mainQueue, ^{
        // 追加任務2
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模擬耗時操做
            NSLog(@"2---%@",[NSThread currentThread]);      // 打印當前線程
        }
    });
    
    dispatch_sync(mainQueue, ^{
        // 追加任務3
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模擬耗時操做
            NSLog(@"3---%@",[NSThread currentThread]);      // 打印當前線程
        }
    });
    
    NSLog(@"CurrentThread end---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"SyncMainQueueTask---end");
    NSLog(@"*********************************************************");
}
複製代碼

執行結果以下:

2019-04-22 16:04:57.238644+0800 GCD(一) 隊列、任務、串行、併發[10673:2238846] CurrentThread begin---<NSThread: 0x6000038ca800>{number = 1, name = main}
2019-04-22 16:04:57.238915+0800 GCD(一) 隊列、任務、串行、併發[10673:2238846] SyncMainQueueTask---begin
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
    frame #0: 0x000000010d45fa19 libdispatch.dylib`__DISPATCH_WAIT_FOR_QUEUE__ + 444
    frame #1: 0x00007ffee542deb0
    frame #2: 0x000000010d45f3f0 libdispatch.dylib`_dispatch_sync_f_slow + 231
  * frame #3: 0x000000010a7cff5a GCD(一) 隊列、任務、串行、併發`-[ViewController executeSyncMainQueueTask:](self=0x00007fb8f25124f0, _cmd="executeSyncMainQueueTask:", sender=0x00007fb8f2515fa0) at ViewController.m:220
    frame #4: 0x000000010e9b9ecb UIKitCore`-[UIApplication sendAction:to:from:forEvent:] + 83
    frame #5: 0x000000010e3f50bd UIKitCore`-[UIControl sendAction:to:forEvent:] + 67
    frame #6: 0x000000010e3f53da UIKitCore`-[UIControl _sendActionsForEvents:withEvent:] + 450
    frame #7: 0x000000010e3f431e UIKitCore`-[UIControl touchesEnded:withEvent:] + 583
    frame #8: 0x000000010e9f50a4 UIKitCore`-[UIWindow _sendTouchesForEvent:] + 2729
    frame #9: 0x000000010e9f67a0 UIKitCore`-[UIWindow sendEvent:] + 4080
    frame #10: 0x000000010e9d4394 UIKitCore`-[UIApplication sendEvent:] + 352
    frame #11: 0x000000010eaa95a9 UIKitCore`__dispatchPreprocessedEventFromEventQueue + 3054
    frame #12: 0x000000010eaac1cb UIKitCore`__handleEventQueueInternal + 5948
    frame #13: 0x000000010bab8721 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    frame #14: 0x000000010bab7f93 CoreFoundation`__CFRunLoopDoSources0 + 243
    frame #15: 0x000000010bab263f CoreFoundation`__CFRunLoopRun + 1263
    frame #16: 0x000000010bab1e11 CoreFoundation`CFRunLoopRunSpecific + 625
    frame #17: 0x00000001141491dd GraphicsServices`GSEventRunModal + 62
    frame #18: 0x000000010e9b881d UIKitCore`UIApplicationMain + 140
    frame #19: 0x000000010a7d03c0 GCD(一) 隊列、任務、串行、併發`main(argc=1, argv=0x00007ffee542ff90) at main.m:14
    frame #20: 0x000000010d4c7575 libdyld.dylib`start + 1
(lldb) 
複製代碼

經過咱們的代碼測驗,能夠看出:

* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
    frame #0: 0x000000010d45fa19 libdispatch.dylib`__DISPATCH_WAIT_FOR_QUEUE__ + 444
複製代碼

應用在主線程同步執行第一個任務時,就會直接crash,咱們同步LLDBbt指定查看函數調用棧,能夠發現,在系統庫libdispatch調用__DISPATCH_WAIT_FOR_QUEUE__函數時,就會產生一個由隊列引發的循環等待致使的crash,這就是咱們常說的Deadlock死鎖,接下里咱們來詳細介紹一下死鎖產生的緣由與注意事項。

GCD中產生死鎖的緣由

- (IBAction)executeSyncMainQueueTask:(UIButton *)sender {
    
    NSLog(@"CurrentThread begin---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"SyncMainQueueTask---begin");
    
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
    dispatch_sync(mainQueue, ^{
        // 追加任務1
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模擬耗時操做
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印當前線程
        }
    });
複製代碼

從上面的代碼咱們能夠看出,當咱們點擊按鈕,調用executeSyncMainQueueTask方法時,這時咱們實際上是在主隊列(串行隊列)提交了一個任務,咱們暫先稱它爲任務0而後咱們又使用dispatch_sync同步執行方法往主隊列中提交了任務1Block,如今咱們來分析一下,爲何這種狀況下會產生死鎖

  1. 先往主隊列(串行隊列)中提交了任務0,而後在任務0執行的過程當中同步地往主隊列中添加了任務1
  2. 主隊列中添加的任務都會在主線程中執行,同時按照串行隊列的特色(任務按序執行),主線程中首先會執行任務0,任務0執行完成以後纔會去執行任務1,可是在任務0執行的過程當中,使用同步方式往主隊列中添加任務1,因爲是使用同步方式,這時主線程會被阻塞,須要任務1完成以後,任務0纔會繼續往下執行。由此,咱們能夠看出,因爲串行隊列的特性,任務1會依賴於任務0的執行完成纔會繼續往下執行,同時因爲同步添加任務的特性(會阻塞當前線程,直到添加的任務執行完成),任務0會依賴於任務1的執行完成。因此,2個任務的執行就會由於相互等待對方的完成,而致使死鎖。

經過上面的分析,咱們能夠看出這裏產生死鎖的一個很重要的緣由就是主隊列是一個串行的隊列(主隊列中只有一條主線程)。若是咱們以下例,在併發隊列中提交,則不會形成死鎖:

dispatch_async(dispatch_get_global_queue(0, 0), ^{ //這裏是爲了保證當前任務是處於併發隊列開闢的線程中,而不是主線程中
  dispatch_sync(dispatch_get_global_queue(0, 0), ^{
      NSLog(@"任務0");
  });
  NSLog(@"任務1");
});
複製代碼

緣由是併發隊列中的任務執行時並行的,因此,任務1並不會一直等待任務0執行完成,纔去執行,而是直接執行完。所以任務0由於任務1的結束,線程阻塞也會被消除,任務0得以繼續執行。

咱們再開看一組示例:

//目前處於主線程中
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
      NSLog(@"任務0");
});
NSLog(@"任務1");
複製代碼

咱們在主線程中,往全局隊列同步提交了Block,由於全局隊列和主隊列是兩個隊列,因此任務1的執行,並不須要等待任務0。因此等任務0結束,任務1也能夠被執行。 固然這裏由於提交Block所在隊列,Block被執行的隊列是徹底不一樣的兩個隊列,因此這裏用串行queue,也是不會死鎖的,到這裏咱們也能夠知道一些同步提交(dispatch_sync)的阻塞機制:

同步提交Block,首先是阻塞的當前提交Block的線程,而在隊列中,同步提交的Block,只會阻塞串行隊列(由串行隊列的同一時間只能執行一個任務的特性決定),並不會阻塞併發隊列,固然dispatch_barrier系列的除外,這個我會在後面的文章中講到,歡迎你們繼續關注個人博客

如今咱們能夠用一句話來總結產生死鎖的緣由就是:

使用同步方式(dispatch_sync)提交一個任務到一個串行隊列時,若是提交這個任務的操做所處的線程,也是處於這個串行隊列,就會引發死鎖

開發中如何避免產生死鎖

  • 不要在主線程中使用同步方式添加任務到主隊列
  • 不要嵌套使用在自定義的串行隊列中,嵌套使用同步方式添加任務到該串行隊列

6種組合使用總結

總結 串行隊列 併發隊列 主隊列
同步添加(sync) 不開闢新線程,在當前線程中串行執行任務 不開闢新線程,在當前線程中串行執行任務 死鎖
異步添加(async) 開闢新線程(1條),串行執行任務 開闢新線程(1/n條),併發執行任務 不開闢新線程,在主線程中順序執行

線程間通訊

#pragma mark - 子線程執行耗時代碼,主線程更新UI
- (IBAction)threadInteraction:(UIButton *)sender {
    
    NSLog(@"CurrentThread begin---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"threadInteraction---begin");
    
    //異步添加任務到全局併發隊列執行耗時操做
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        //執行耗時任務
        for (int i = 0; i < 2; ++i) {
            [NSThread sleepForTimeInterval:2];              // 模擬耗時操做
            NSLog(@"1---%@",[NSThread currentThread]);      // 打印當前線程
        }
        
        //回到主線程更新UI
        dispatch_sync(dispatch_get_main_queue(), ^{
            
            //Do something here to update UI
            
        });
    });
    
    NSLog(@"CurrentThread end---%@",[NSThread currentThread]);  // 打印當前線程
    NSLog(@"threadInteraction---end");
    NSLog(@"*********************************************************");
}
複製代碼

執行結果以下:

2019-04-22 18:47:54.836027+0800 GCD(一) 隊列、任務、串行、併發[13229:2669381] CurrentThread begin---<NSThread: 0x6000038e9400>{number = 1, name = main}
2019-04-22 18:47:54.836217+0800 GCD(一) 隊列、任務、串行、併發[13229:2669381] threadInteraction---begin
2019-04-22 18:47:54.836436+0800 GCD(一) 隊列、任務、串行、併發[13229:2669381] CurrentThread end---<NSThread: 0x6000038e9400>{number = 1, name = main}
2019-04-22 18:47:54.836619+0800 GCD(一) 隊列、任務、串行、併發[13229:2669381] threadInteraction---end
2019-04-22 18:47:54.836761+0800 GCD(一) 隊列、任務、串行、併發[13229:2669381] *********************************************************
2019-04-22 18:47:56.839416+0800 GCD(一) 隊列、任務、串行、併發[13229:2669427] 1---<NSThread: 0x6000038b6d40>{number = 4, name = (null)}
2019-04-22 18:47:58.840646+0800 GCD(一) 隊列、任務、串行、併發[13229:2669427] 1---<NSThread: 0x6000038b6d40>{number = 4, name = (null)}

複製代碼

在日常的開發中,咱們最經常使用的就是提交一個任務到全局併發隊列取執行一些比較耗時的操做(好比,文件下載、文件上傳、圖片解碼、數據庫操做、IO讀寫),而後再切回到主線程去更新UI,蘋果官方限定了更新UI的操做只能在主線程中執行,因此,咱們最後仍是要回到主線程去處理咱們的UI交互。

本文到這裏已經基本結束,在接下來的文章中,我將會繼續講解GCD多線程編程的另外幾個知識點,也是咱們平時開發中實際很常常會用到的,如dispatch_barrierdispatch_groupdispatch_semaphore線程安全的相關內容

若是文中有錯誤的地方,或者與你的想法相悖的地方,請在評論區告知我,我會繼續改進,若是你以爲這個篇文章總結的還不錯,麻煩動動小手,給個人文章與Git代碼樣例點個✨

相關文章
相關標籤/搜索