iOS-底層原理 26:GCD 之 函數(shù)與隊列

iOS 底層原理 文章匯總

本文的主要目的是理解不同隊列與不同函數(shù)之間組合的情況

GCD簡介

  • GCD全稱是Grand Central Dispatch

  • 純C語言,提供例如非常強(qiáng)大的函數(shù)

GCD優(yōu)勢

  • GCD是蘋果公司為多核的并行運(yùn)算提出的解決方案

  • GCD會自動利用更多的CPU內(nèi)核(比如雙核、四核)

  • GCD會自動管理線程的生命周期(創(chuàng)建線程、調(diào)度任務(wù)、銷毀線程)

  • 程序員只需要告訴GCD想要執(zhí)行什么任務(wù),不需要編寫任何線程管理代碼

【重點】用一句話總結(jié)GCD就是:將任務(wù)添加到隊列,并指定任務(wù)執(zhí)行的函數(shù)

GCD核心

在日常開發(fā)中,GCD一般寫成下面這種形式

 dispatch_async( dispatch_queue_create("com.CJL.Queue", NULL), ^{
   NSLog(@"GCD基本使用");
});

將上述代碼拆分,方便我們來理解GCD核心 主要是由 任務(wù) + 隊列 + 函數(shù) 構(gòu)成

//********GCD基礎(chǔ)寫法********
//創(chuàng)建任務(wù)
dispatch_block_t block = ^{
    NSLog(@"hello GCD");
};

//創(chuàng)建串行隊列
dispatch_queue_t queue = dispatch_queue_create("com.CJL.Queue", NULL);

//將任務(wù)添加到隊列,并指定函數(shù)執(zhí)行
dispatch_async(queue, block);
  • 使用dispatch_block_t創(chuàng)建任務(wù)
  • 使用dispatch_queue_t創(chuàng)建隊列
  • 將任務(wù)添加到隊列,并指定執(zhí)行任務(wù)的函數(shù)dispatch_async

注意

這里的任務(wù)是指執(zhí)行操作的意思,在使用dispatch_block_t創(chuàng)建任務(wù)時,主要有以下兩點說明

  • 任務(wù)使用block封裝

  • 任務(wù)的block沒有參數(shù)沒有返回值

函數(shù)與隊列

函數(shù)

在GCD中執(zhí)行任務(wù)的方式有兩種,同步執(zhí)行和異步執(zhí)行,分別對應(yīng) 同步函數(shù)dispatch_sync異步函數(shù)dispatch_async,兩者對比如下

  • 同步執(zhí)行,對應(yīng)同步函數(shù)dispatch_sync
    • 必須等待當(dāng)前語句執(zhí)行完畢,才會執(zhí)行下一條語句

    • 不會開啟線程,即不具備開啟新線程的能力

    • 在當(dāng)前線程中執(zhí)行block任務(wù)

  • 異步執(zhí)行,對應(yīng)異步函數(shù)dispatch_async
    • 不用等待當(dāng)前語句執(zhí)行完畢,就可以執(zhí)行下一條語句

    • 開啟線程執(zhí)行block任務(wù),即具備開啟新線程的能力(但并不一定開啟新線程,這個與任務(wù)所指定的隊列類型有關(guān))

    • 異步 是 多線程 的代名詞

所以,綜上所述,兩種執(zhí)行方式的主要區(qū)別有兩點:

  • 是否等待隊列的任務(wù)執(zhí)行完畢

  • 是否具備開啟新線程的能力

隊列

串行隊列 和 并發(fā)隊列

多線程中所說的隊列(Dispatch Queue)是指執(zhí)行任務(wù)的等待隊列,即用來存放任務(wù)的隊列。隊列是一種特殊的線性表,遵循先進(jìn)先出(FIFO)原則,即新任務(wù)總是被插入到隊尾,而任務(wù)的讀取從隊首開始讀取。每讀取一個任務(wù),則動隊列中釋放一個任務(wù),如下圖所示

隊列圖示

在GCD中,隊列主要分為串行隊列(Serial Dispatch Queue)并發(fā)隊列(Concurrent Dispatch Queue)兩種,如下圖所示

串行 and 并行

  • 串行隊列:每次只有一個任務(wù)被執(zhí)行,等待上一個任務(wù)執(zhí)行完畢再執(zhí)行下一個,即只開啟一個線程(通俗理解:同一時刻只調(diào)度一個任務(wù)執(zhí)行)
    • 使用dispatch_queue_create("xxx", DISPATCH_QUEUE_SERIAL);創(chuàng)建串行隊列

    • 其中的DISPATCH_QUEUE_SERIAL也可以使用NULL表示,這兩種均表示 默認(rèn)的串行隊列

// 串行隊列的獲取方法
dispatch_queue_t serialQueue1 = dispatch_queue_create("com.CJL.Queue", NULL);
    dispatch_queue_t serialQueue2 = dispatch_queue_create("com.CJL.Queue", DISPATCH_QUEUE_SERIAL);
  • 并發(fā)隊列:一次可以并發(fā)執(zhí)行多個任務(wù),即開啟多個線程,并同時執(zhí)行任務(wù)(通俗理解:同一時刻可以調(diào)度多個任務(wù)執(zhí)行)
    • 使用dispatch_queue_create("xxx", DISPATCH_QUEUE_CONCURRENT);創(chuàng)建并發(fā)隊列

    • 注意:并發(fā)隊列的并發(fā)功能只有在異步函數(shù)下才有效

// 并發(fā)隊列的獲取方法
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.CJL.Queue", DISPATCH_QUEUE_CONCURRENT);

主隊列 和 全局并發(fā)隊列

在GCD中,針對這兩種隊列,分別提供了主隊列(Main Dispatch Queue)全局并發(fā)隊列(Global Dispatch Queue)

  • 主隊列(Main Dispatch Queue):GCD中提供的特殊的串行隊列
    • 專門用來在主線程上調(diào)度任務(wù)的串行隊列,依賴于主線程、主Runloop,在main函數(shù)調(diào)用之前自動創(chuàng)建

    • 不會開啟線程

    • 如果當(dāng)前主線程正在有任務(wù)執(zhí)行,那么無論主隊列中當(dāng)前被添加了什么任務(wù),都不會被調(diào)度

    • 使用dispatch_get_main_queue()獲得主隊列

    • 通常在返回主線程 更新UI時使用

//主隊列的獲取方法
dispatch_queue_t mainQueue = dispatch_get_main_queue();
  • 全局并發(fā)隊列(Global Dispatch Queue):GCD提供的默認(rèn)的并發(fā)隊列
    • 為了方便程序員的使用,蘋果提供了全局隊列

    • 在使用多線程開發(fā)時,如果對隊列沒有特殊需求,在執(zhí)行異步任務(wù)時,可以直接使用全局隊列

    • 使用dispatch_get_global_queue獲取全局并發(fā)隊列,最簡單的是dispatch_get_global_queue(0, 0)

      • 第一個參數(shù)表示隊列優(yōu)先級,默認(rèn)優(yōu)先級為DISPATCH_QUEUE_PRIORITY_DEFAULT=0,在ios9之后,已經(jīng)被服務(wù)質(zhì)量(quality-of-service)取代

      • 第二個參數(shù)使用0

//全局并發(fā)隊列的獲取方法
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);

//優(yōu)先級從高到低(對應(yīng)的服務(wù)質(zhì)量)依次為
- DISPATCH_QUEUE_PRIORITY_HIGH       -- QOS_CLASS_USER_INITIATED
- DISPATCH_QUEUE_PRIORITY_DEFAULT    -- QOS_CLASS_DEFAULT
- DISPATCH_QUEUE_PRIORITY_LOW        -- QOS_CLASS_UTILITY
- DISPATCH_QUEUE_PRIORITY_BACKGROUND -- QOS_CLASS_BACKGROUND

全局并發(fā)隊列 + 主隊列 配合使用

在日常開發(fā)中,全局隊列+并發(fā)并列一般是這樣配合使用的

//主隊列 + 全局并發(fā)隊列的日常使用
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //執(zhí)行耗時操作
        dispatch_async(dispatch_get_main_queue(), ^{
            //回到主線程進(jìn)行UI操作
        });
    });

函數(shù)與隊列的不同組合

串行隊列 + 同步函數(shù)

【任務(wù)按順序執(zhí)行】:任務(wù)一個接一個的在當(dāng)前線程執(zhí)行,不會開辟新線程

串行隊列 + 同步函數(shù)

串行隊列 + 異步函數(shù)

【任務(wù)按順序執(zhí)行】:任務(wù)一個接一個的執(zhí)行,會開辟新線程

串行隊列 + 異步函數(shù)

并發(fā)隊列 + 同步函數(shù)

【任務(wù)按順序執(zhí)行】:任務(wù)一個接一個的執(zhí)行,不開辟線程

并發(fā)隊列 + 同步函數(shù)

并發(fā)隊列 + 異步函數(shù)

【任務(wù)亂序執(zhí)行】:任務(wù)執(zhí)行無順序,會開辟新線程

并發(fā)隊列 + 異步函數(shù)

主隊列 + 同步函數(shù)

【造成死鎖】:任務(wù)相互等待,造成死鎖

主隊列 + 同步函數(shù)

造成死鎖的原因分析如下:

  • 主隊列有兩個任務(wù),順序為:NSLog任務(wù) - 同步block

  • 執(zhí)行NSLog任務(wù)后,執(zhí)行同步Block,會將任務(wù)1(即i=1時)加入到主隊列,主隊列順序為:NSLog任務(wù) - 同步block - 任務(wù)1

  • 任務(wù)1的執(zhí)行需要等待同步block執(zhí)行完畢才會執(zhí)行,而同步block的執(zhí)行需要等待任務(wù)1執(zhí)行完畢,所以就造成了任務(wù)互相等待的情況,即造成死鎖崩潰

死鎖現(xiàn)象

  • 主線程因為你同步函數(shù)的原因等著先執(zhí)行任務(wù)

  • 主隊列等著主線程的任務(wù)執(zhí)行完畢再執(zhí)行自己的任務(wù)

  • 主隊列和主線程相互等待會造成死鎖

主隊列 + 異步函數(shù)

【任務(wù)按順序執(zhí)行】:任務(wù)一個接一個的執(zhí)行,不開辟線程

主隊列 + 異步函數(shù)

全局并發(fā)隊列 + 同步函數(shù)

【任務(wù)按順序執(zhí)行】:任務(wù)一個接一個的執(zhí)行,不開辟新線程

全局并發(fā)隊列 + 同步函數(shù)

全局并發(fā)隊列 + 異步函數(shù)

【任務(wù)亂序執(zhí)行】:任務(wù)亂序執(zhí)行,會開辟新線程

全局并發(fā)隊列 + 異步函數(shù)

總結(jié)

函數(shù)\隊列 串行隊列 并發(fā)隊列 主隊列 全局并發(fā)隊列
同步函數(shù) 順序執(zhí)行,不開辟線程 順序執(zhí)行,不開辟線程 死鎖 順序執(zhí)行,不開辟線程
異步函數(shù) 順序執(zhí)行,開辟線程 亂序執(zhí)行,開辟線程 順序執(zhí)行,不開辟線程 亂序執(zhí)行,開辟線程

相關(guān)面試題解析

【面試題 - 1】異步函數(shù)+并行隊列

下面代碼的輸出順序是什么?

- (void)interview01{
    //并行隊列
    dispatch_queue_t queue = dispatch_queue_create("com.CJL.Queue", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    // 耗時
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_async(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}
----------打印結(jié)果-----------
輸出順序為:1 5 2 4 3
  • 異步函數(shù)不會阻塞主隊列,會開辟新線程執(zhí)行異步任務(wù)
    打印結(jié)果

代碼分析

如下圖所示,紅線表示任務(wù)的執(zhí)行順序


分析圖示
  • 主線程的任務(wù)隊列為:任務(wù)1、異步block1、任務(wù)5,其中異步block1會比較耗費(fèi)性能,任務(wù)1和任務(wù)5的任務(wù)復(fù)雜度是相同的,所以任務(wù)1和任務(wù)5優(yōu)先于異步block1執(zhí)行

  • 異步block1中,任務(wù)隊列為:任務(wù)2、異步block2、任務(wù)4,其中block2相對比較耗費(fèi)性能,任務(wù)2任務(wù)4是復(fù)雜度一樣,所以任務(wù)2和任務(wù)4優(yōu)先于block2執(zhí)行

  • 最后執(zhí)行block2中的任務(wù)3

  • 在極端情況下,可能出現(xiàn) 任務(wù)2先于任務(wù)1任務(wù)5執(zhí)行,原因是出現(xiàn)了當(dāng)前主線程卡頓或者 延遲的情況

代碼修改

  • 【修改1】:將并行隊列 改成 串行隊列,對結(jié)果沒有任何影響,順序仍然是 1 5 2 4 3

  • 【修改2】:在任務(wù)5之前,休眠2s,即sleep(2),執(zhí)行的順序為:1 2 4 3 5,原因是因為I/O的打印,相比于休眠2s,復(fù)雜度更簡單,所以異步block1 會先于任務(wù)5執(zhí)行。當(dāng)然如果主隊列堵塞,會出現(xiàn)其他的執(zhí)行順序

【面試題 - 2】異步函數(shù)嵌套同步函數(shù) + 并發(fā)隊列

下面代碼的輸出順序是什么?

- (void)interview02{
    //并發(fā)隊列
    dispatch_queue_t queue = dispatch_queue_create("com.CJL.Queue", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    //異步函數(shù)
    dispatch_async(queue, ^{
        NSLog(@"2");
        //同步函數(shù)
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}

----------打印結(jié)果-----------
輸出順序為:1 5 2 3 4

分析

  • 任務(wù)1 和 任務(wù)5的分析同前面一致,執(zhí)行順序為 任務(wù)1 任務(wù)5 異步block

  • 在異步block中,首先執(zhí)行任務(wù)2,然后走到同步block,由于同步函數(shù)會阻塞主線程,所以任務(wù)4需要等待任務(wù)3執(zhí)行完成后,才能執(zhí)行,所以異步block中的執(zhí)行順序是:任務(wù)2 任務(wù)3 任務(wù)4

打印結(jié)果

【面試題 - 3】異步函數(shù)嵌套同步函數(shù) + 串行隊列(即同步隊列)

下面代碼的執(zhí)行順序是什么?會出現(xiàn)什么情況?為什么?

- (void)interview03{
    // 同步隊列
    dispatch_queue_t queue = dispatch_queue_create("com.CJL.Queue", NULL);
    NSLog(@"1");
    // 異步函數(shù)
    dispatch_async(queue, ^{
        NSLog(@"2");
        // 同步函數(shù)
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}

----------打印結(jié)果-----------
輸出順序為:1 5 2 死鎖崩潰

分析

如下圖所示,紅色表示任務(wù)執(zhí)行順序,黑色虛線表示等待


分析圖示
  • 首先執(zhí)行任務(wù)1,接下來是異步block,并不會阻塞主線程,相比任務(wù)5而言,復(fù)雜度更高,所以優(yōu)先執(zhí)行任務(wù)5,在執(zhí)行異步block

  • 異步block中,先執(zhí)行任務(wù)2,接下來是同步block同步函數(shù)會阻塞線程,所以執(zhí)行任務(wù)4需要等待任務(wù)3執(zhí)行完成,而任務(wù)3的執(zhí)行,需要等待異步block執(zhí)行完成,相當(dāng)于任務(wù)3等待任務(wù)4完成

  • 所以就造成了任務(wù)4等待任務(wù)3,任務(wù)3等待任務(wù)4,即互相等待的局面,就會造成死鎖,這里有個重點是關(guān)鍵的堆棧 slow

    打印結(jié)果

修改

去掉任務(wù)4,執(zhí)行順序是什么?

  • 還是會死鎖,因為任務(wù)3等待的是異步block執(zhí)行完畢,而異步block等待任務(wù)3

【面試題 - 4 - 新浪】 異步函數(shù) + 同步函數(shù) + 并發(fā)隊列

下面代碼的執(zhí)行順序是什么?(答案是 AC)
A: 1230789
B: 1237890
C: 3120798
D: 2137890

- (void)interview04{
    //并發(fā)隊列
    dispatch_queue_t queue = dispatch_queue_create("com.CJL.Queue", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{ // 耗時
        NSLog(@"1");
    });
    dispatch_async(queue, ^{
        NSLog(@"2");
    });
    
    // 同步
    dispatch_sync(queue, ^{
        NSLog(@"3");
    });
    
    NSLog(@"0");

    dispatch_async(queue, ^{
        NSLog(@"7");
    });
    dispatch_async(queue, ^{
        NSLog(@"8");
    });
    dispatch_async(queue, ^{
        NSLog(@"9");
    });
}

----------打印結(jié)果-----------
輸出順序為:(1 2 3 無序)0(7 8 9 無序),可以確定的是 0 一定在3之后,在789之前

分析

  • 任務(wù)1任務(wù)2由于是異步函數(shù)+并發(fā)隊列,會開啟線程,所以沒有固定順序
  • 任務(wù)7、任務(wù)8、任務(wù)9同理,會開啟線程,所以沒有固定順序
  • 任務(wù)3同步函數(shù)+并發(fā)隊列,同步函數(shù)會阻塞主線程,但是也只會阻塞0,所以,可以確定的是 0一定在3之后,在789之前

以下是不同的執(zhí)行順序的打印


不同的執(zhí)行順序

【面試題 - 5 - 美團(tuán)】下面代碼中,隊列的類型有幾種?

//串行隊列 - Serial Dispatch Queue
dispatch_queue_t serialQueue = dispatch_queue_create("com.CJL.Queue", NULL);
    
//并發(fā)隊列 - Concurrent Dispatch Queue
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.CJL.Queue", DISPATCH_QUEUE_CONCURRENT);
    
//主隊列 - Main Dispatch Queue
dispatch_queue_t mainQueue = dispatch_get_main_queue();
    
//全局并發(fā)隊列 - Global Dispatch Queue
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);

隊列總共有兩種: 并發(fā)隊列串行隊列

  • 串行隊列:serialQueue、mainQueue
  • 并發(fā)隊列:concurrentQueue、globalQueue
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容