iOS-底層探索22:GCD上-函數(shù)與隊列&面試題

iOS 底層探索 文章匯總

目錄


一、GCD簡介

  1. 什么是GCD:
  • 全稱是Grand Central Dispatch
  • 純C語言锚扎,提供了非常多強大的函數(shù)
  • 將任務(wù)添加到隊列吞瞪,并且指定執(zhí)行任務(wù)的函數(shù)
  1. GCD的優(yōu)勢:
  • GCD是蘋果公司為多核的并行運算提出的解決方案
  • GCD會自動利用更多的CPU內(nèi)核(比如雙核、四核)
  • GCD會自動管理線程的生命周期(創(chuàng)建線程驾孔、調(diào)度任務(wù)芍秆、銷毀線程)
  • 程序員只需要告訴GCD想要執(zhí)行什么任務(wù),不需要編寫任何線程管理代碼

二翠勉、任務(wù)妖啥、函數(shù)、隊列

  1. 任務(wù)使用block封裝
  2. 任務(wù)的block沒有參數(shù)也沒有返回值
  3. 執(zhí)行任務(wù)的函數(shù)
    • 異步dispatch async函數(shù)
      不用等待當前語句執(zhí)行完畢对碌,就可以執(zhí)行下一條語句
      會開啟線程執(zhí)行block的任務(wù)
      異步是多線程的代名詞
  • 同步dispatch sync函數(shù)
    必須等待當前語句執(zhí)行完畢荆虱,才會執(zhí)行下一條語句
    不會開啟線程
    在當前線程執(zhí)行block的任務(wù)
  1. 隊列
    隊列用來調(diào)度任務(wù),任務(wù)都做線程中執(zhí)行
    串行隊列和并發(fā)隊列都符合FIFO原則(先進先出并不是先進先執(zhí)行完成)


  1. 隊列和函數(shù)
    異步函數(shù)+并發(fā)隊列 開啟多條新的子線程、并發(fā)
    異步函數(shù)+串行隊列 為所有串行隊列中的任務(wù)僅開啟一個新的子線程怀读、串行
    同步函數(shù)+并發(fā)隊列 同步函數(shù)中均不開啟新的子線程诉位、串行
    同步函數(shù)+串行隊列 同上...


  1. 主隊列和全局隊列

主隊列

  • 專門用來在主線程上調(diào)度任務(wù)的串行隊列
  • 不會開啟線程
  • 如果當前主線程正在有任務(wù)執(zhí)行,那么無論主隊列中當前被添加了什么任務(wù)菜枷,都不會被調(diào)度
  • dispatch get main queue();

全局隊列

  • 為了方便程序員的使用苍糠,蘋果提供了全局隊列dispatch_get_global queue(0, 0)
  • 全局隊列是一個并發(fā)隊列
  • 在使用多線程開發(fā)時,如果對隊列沒有特殊需求啤誊,在執(zhí)行異步任務(wù)時椿息,可以直接使用全局隊列

三、GCD相關(guān)面試題

  1. 下面代碼輸出什么坷衍?
- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"1");
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"2");
    });
    NSLog(@"3");
}

這個程序就是典型的死鎖寝优,可以看到,只打印了1枫耳,就再也沒有響應(yīng)了乏矾,已經(jīng)造成了GCD死鎖。為什么會這樣呢迁杨?讓我們來解讀一下這段程序的運行順序:首先會打印1钻心,然后將主隊列和一個block傳入GCD同步函數(shù)dispatch_sync中,等待sync函數(shù)執(zhí)行铅协,直到它返回捷沸,才會執(zhí)行打印3的語句『罚可是痒给,竟然沒有反應(yīng)了?block中的2沒有被打印出來骏全,viewDidLoad()中的3也沒有被打印出來苍柏。也就是說,block沒有得到執(zhí)行的機會姜贡,viewDidLoad也沒有繼續(xù)執(zhí)行下去试吁。為什么block不執(zhí)行呢?因為viewDidLoad也是執(zhí)行在主隊列的楼咳,它是正在被執(zhí)行的任務(wù)熄捍,也就是說,viewDidLoad()是主隊列的隊頭母怜。主隊列是串行隊列余耽,任務(wù)不能并發(fā)執(zhí)行,同時只能有一個任務(wù)在執(zhí)行糙申,也就是隊頭的任務(wù)才能被出列執(zhí)行宾添。我們現(xiàn)在被執(zhí)行的任務(wù)是viewDidLoad(),然后我們又將block入列到同一個隊列柜裸,它比viewDidLoad()后入列缕陕,遵循先進先出的原理,它必須等到viewDidLoad()執(zhí)行完疙挺,才能被執(zhí)行扛邑。 但是,dispatch_sync函數(shù)的特性是铐然,等待block被執(zhí)行完畢蔬崩,才會返回,因此搀暑,只要block一天不被執(zhí)行沥阳,它就一天不返回。我們知道自点,內(nèi)部方法不返回桐罕,外部方法是不會執(zhí)行下一行命令的。不等到sync函數(shù)返回桂敛,viewDidLoad打死也不會執(zhí)行print End的語句功炮,因此,viewDidLoad()一直沒有執(zhí)行完畢术唬。block在等待著viewDidLoad()執(zhí)行完畢薪伏,它才能上,sync函數(shù)在等待著block執(zhí)行完畢粗仓,它才能返回嫁怀,viewDidLoad()在等待著sync函數(shù)返回,它才能執(zhí)行完畢借浊。這樣的三方循環(huán)等待關(guān)系眶掌,就造成了死鎖。

參考:理解GCD死鎖
關(guān)于GCD的例題:iOS面試題:阿里-P6一面-參考思路

總結(jié):同步串行必阻塞當前隊列(不一定會死鎖)


  1. 下面代碼輸出什么巴碗?
- (void)viewDidLoad {
    [super viewDidLoad];
 
    dispatch_queue_t queue = dispatch_queue_create("differ", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_async(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    //sleep(2);這里會導(dǎo)致打印5會后執(zhí)行
    NSLog(@"5");
}
- (void)viewDidLoad {
    [super viewDidLoad];
    
    dispatch_queue_t queue = dispatch_queue_create("differ", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}

答案:15243朴爬、15234

大廠面試題:

- (void)viewDidLoad {
    [super viewDidLoad];
 
    dispatch_queue_t queue = dispatch_queue_create("differ", DISPATCH_QUEUE_SERIAL);
    NSLog(@"1");
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_async(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}
- (void)viewDidLoad {
    [super viewDidLoad];
    
    dispatch_queue_t queue = dispatch_queue_create("differ", DISPATCH_QUEUE_SERIAL);
    NSLog(@"1");
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}

答案:15243、152死鎖(沒有NSLog(@"4");也是同樣的)

注意: #define DISPATCH_QUEUE_SERIAL NUL


  1. 下面代碼有可能輸出什么橡淆?
- (void)viewDidLoad {
    [super viewDidLoad];
    
    dispatch_queue_t queue = dispatch_queue_create("com.differ.cn", 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");
    });
}

A: 1230789
B: 1237890
C: 3120798
D: 2137890

分析:

![](https://upload-images.jianshu.io/upload_images/2987980-b6ff3234f735ce80.png?imageMogr2/auto- orient/strip%7CimageView2/2/w/1240)

  • 打印1召噩、2、3均在同一并發(fā)隊列中且打印1逸爵、2為異步函數(shù)具滴,所以1、2师倔、3的打印順序不是確定的构韵。
  • 重點在于打印3的同步函數(shù),由于其在main串行隊列中根據(jù)FIFO,會先執(zhí)行完成打印3然后再執(zhí)行打印0疲恢。(同步函數(shù)會阻塞當前隊列)
  • 執(zhí)行打印0后再將打印7凶朗、8、9任務(wù)添加到并發(fā)隊列中显拳。所以3一定在0之前棚愤,7、8杂数、9一定在0之后宛畦,1、2位置不確定揍移。

答案:AC


四次和、GCD底層分析

1、尋找GCD底層庫

添加dispatch_sync符號斷點

運行代碼:

所以GCD的底層源碼在libdispatch.dylib庫中

GCD源碼初始化方法libdispatch_init(void)

2那伐、隊列是如何創(chuàng)建的斯够?串行隊列和并發(fā)隊列創(chuàng)建時如何區(qū)分的?

隊列創(chuàng)建方法:dispatch_queue_create

dispatch_queue_t
dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)
{
    return _dispatch_lane_create_with_target(label, attr,
            DISPATCH_TARGET_QUEUE_DEFAULT, true);
}
DISPATCH_NOINLINE
static dispatch_queue_t
_dispatch_lane_create_with_target(const char *label, dispatch_queue_attr_t dqa,
        dispatch_queue_t tq, bool legacy)
{
    // dqai 創(chuàng)建:根據(jù)傳入的串行/并發(fā)參數(shù)創(chuàng)建
    dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);

    // Step 1: Normalize arguments (qos, overcommit, tq)

    ...

    // Step 2: Initialize the queue

    ...
    const void *vtable;
    dispatch_queue_flags_t dqf = legacy ? DQF_MUTABLE : 0;
    // 確定串行隊列/并發(fā)隊列的類名
    if (dqai.dqai_concurrent) {
        // OS_dispatch_queue_concurrent-拼接類名
        vtable = DISPATCH_VTABLE(queue_concurrent);
    } else {
        vtable = DISPATCH_VTABLE(queue_serial);
    }

...

    dispatch_lane_t dq = _dispatch_object_alloc(vtable,
            sizeof(struct dispatch_lane_s)); // alloc喧锦、確定isa指向的類名
    //根據(jù)qai.dqai_concurrent區(qū)分是串行隊列還是并發(fā)隊列
    _dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ?
            DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
            (dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0)); // init

    dq->dq_label = label;
    dq->dq_priority = _dispatch_priority_make((dispatch_qos_t)dqai.dqai_qos,
            dqai.dqai_relpri);
    if (overcommit == _dispatch_queue_attr_overcommit_enabled) {
        dq->dq_priority |= DISPATCH_PRIORITY_FLAG_OVERCOMMIT;
    }
    if (!dqai.dqai_inactive) {
        _dispatch_queue_priority_inherit_from_target(dq, tq);
        _dispatch_lane_inherit_wlh_from_target(dq, tq);
    }
    _dispatch_retain(tq);
    dq->do_targetq = tq;
    _dispatch_object_debug(dq, "%s", __func__);
    return _dispatch_trace_queue_create(dq)._dq;
}

dqai的創(chuàng)建方法中我們可以發(fā)現(xiàn)只要隊列創(chuàng)建傳入的參數(shù)不是DISPATCH_QUEUE_CONCURRENT那就創(chuàng)建并發(fā)隊列

dispatch_queue_attr_info_t
_dispatch_queue_attr_to_info(dispatch_queue_attr_t dqa)
{
    dispatch_queue_attr_info_t dqai = { };

    if (!dqa) return dqai;

#if DISPATCH_VARIANT_STATIC
    if (dqa == &_dispatch_queue_attr_concurrent) { // null 默認
        dqai.dqai_concurrent = true;
        return dqai;
    }
#endif

....
}
#define DISPATCH_QUEUE_CONCURRENT \
        DISPATCH_GLOBAL_OBJECT(dispatch_queue_attr_t, \
        _dispatch_queue_attr_concurrent)

通過隊列queue的創(chuàng)建過程我們可以看到queue也是一個對象读规,存在alloc、init燃少、isa

DISPATCH_QUEUE_WIDTH_MAX決定了當前隊列能開辟多大:

#define DISPATCH_QUEUE_WIDTH_FULL           0x1000ull
#define DISPATCH_QUEUE_WIDTH_POOL (DISPATCH_QUEUE_WIDTH_FULL - 1)
#define DISPATCH_QUEUE_WIDTH_MAX  (DISPATCH_QUEUE_WIDTH_FULL - 2)

DISPATCH_QUEUE_WIDTH_POOL-全局并發(fā)隊列寬度:4096-1
DISPATCH_QUEUE_WIDTH_MAX- 一般并發(fā)隊列寬度: 4096-2


3束亏、異步函數(shù)底層分析:dispatch_async

#ifdef __BLOCKS__
void
dispatch_async(dispatch_queue_t dq, dispatch_block_t work)
{
    dispatch_continuation_t dc = _dispatch_continuation_alloc();
    uintptr_t dc_flags = DC_FLAG_CONSUME;
    dispatch_qos_t qos;

    // 任務(wù)包裝器 - 保存 block
    qos = _dispatch_continuation_init(dc, dq, work, 0, dc_flags);
    _dispatch_continuation_async(dq, dc, qos, dc->dc_flags);
}
#endif

dispatch_async方法中主要調(diào)用兩個方法:
_dispatch_continuation_init_dispatch_continuation_async
先介紹_dispatch_continuation_init方法_dispatch_continuation_async在下一篇文章分析

DISPATCH_ALWAYS_INLINE
static inline dispatch_qos_t
_dispatch_continuation_init(dispatch_continuation_t dc,
        dispatch_queue_class_t dqu, dispatch_block_t work,
        dispatch_block_flags_t flags, uintptr_t dc_flags)
{
    void *ctxt = _dispatch_Block_copy(work);

    dc_flags |= DC_FLAG_BLOCK | DC_FLAG_ALLOCATED;
    if (unlikely(_dispatch_block_has_private_data(work))) {
        dc->dc_flags = dc_flags;
        dc->dc_ctxt = ctxt;
        // will initialize all fields but requires dc_flags & dc_ctxt to be set
        return _dispatch_continuation_init_slow(dc, dqu, flags);
    }

    dispatch_function_t func = _dispatch_Block_invoke(work);
    if (dc_flags & DC_FLAG_CONSUME) {
        func = _dispatch_call_block_and_release;//調(diào)用Block的func
    }
    return _dispatch_continuation_init_f(dc, dqu, ctxt, func, flags, dc_flags);
}
DISPATCH_ALWAYS_INLINE
static inline dispatch_qos_t
_dispatch_continuation_init_f(dispatch_continuation_t dc,
        dispatch_queue_class_t dqu, void *ctxt, dispatch_function_t f,
        dispatch_block_flags_t flags, uintptr_t dc_flags)
{
    pthread_priority_t pp = 0;
    dc->dc_flags = dc_flags | DC_FLAG_ALLOCATED;
    dc->dc_func = f;// 保存調(diào)用Block的func
    dc->dc_ctxt = ctxt;// 保存Block
    // in this context DISPATCH_BLOCK_HAS_PRIORITY means that the priority
    // should not be propagated, only taken from the handler if it has one
    if (!(flags & DISPATCH_BLOCK_HAS_PRIORITY)) {
        pp = _dispatch_priority_propagate();
    }
    _dispatch_continuation_voucher_set(dc, flags);
    return _dispatch_continuation_priority_set(dc, dqu, pp, flags);
}

打印出Block調(diào)用的堆棧:

因此的確是_dispatch_call_block_and_release方法調(diào)用了Block

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市阵具,隨后出現(xiàn)的幾起案子碍遍,更是在濱河造成了極大的恐慌,老刑警劉巖阳液,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件怕敬,死亡現(xiàn)場離奇詭異,居然都是意外死亡帘皿,警方通過查閱死者的電腦和手機东跪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鹰溜,“玉大人虽填,你說我怎么就攤上這事〔芏” “怎么了斋日?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長墓陈。 經(jīng)常有香客問我恶守,道長第献,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任兔港,我火速辦了婚禮庸毫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘押框。我一直安慰自己,他們只是感情好理逊,可當我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布橡伞。 她就那樣靜靜地躺著,像睡著了一般晋被。 火紅的嫁衣襯著肌膚如雪兑徘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天挂脑,我揣著相機與錄音,去河邊找鬼欲侮。 笑死崭闲,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的威蕉。 我是一名探鬼主播刁俭,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼韧涨!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤怖喻,失蹤者是張志新(化名)和其女友劉穎失乾,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體娩贷,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡第晰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了彬祖。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片但荤。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖涧至,靈堂內(nèi)的尸體忽然破棺而出腹躁,到底是詐尸還是另有隱情,我是刑警寧澤南蓬,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布纺非,位于F島的核電站哑了,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏烧颖。R本人自食惡果不足惜弱左,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望炕淮。 院中可真熱鬧拆火,春花似錦、人聲如沸涂圆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽润歉。三九已至模狭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間踩衩,已是汗流浹背嚼鹉。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留驱富,地道東北人锚赤。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像褐鸥,于是被迫代替她去往敵國和親宴树。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,779評論 2 354