iOS-底层探索22:GCD上-函数与队列&面试题

iOS 底层探索 文章汇总

目录


一、GCD简介

  1. 什么是GCD:
  • 全称是Grand Central Dispatch
  • 纯C语言,提供了非常多强大的函数
  • 将任务添加到队列,并且指定执行任务的函数
  1. GCD的优势:
  • GCD是苹果公司为多核的并行运算提出的解决方案
  • GCD会自动利用更多的CPU内核(比如双核、四核)
  • GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
  • 程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码

二、任务、函数、队列

  1. 任务使用block封装
  2. 任务的block没有参数也没有返回值
  3. 执行任务的函数
    • 异步dispatch async函数
      不用等待当前语句执行完毕,就可以执行下一条语句
      会开启线程执行block的任务
      异步是多线程的代名词
  • 同步dispatch sync函数
    必须等待当前语句执行完毕,才会执行下一条语句
    不会开启线程
    在当前线程执行block的任务
  1. 队列
    队列用来调度任务,任务都做线程中执行
    串行队列和并发队列都符合FIFO原则(先进先出并不是先进先执行完成)


  1. 队列和函数
    异步函数+并发队列 开启多条新的子线程、并发
    异步函数+串行队列 为所有串行队列中的任务仅开启一个新的子线程、串行
    同步函数+并发队列 同步函数中均不开启新的子线程、串行
    同步函数+串行队列 同上...


  1. 主队列和全局队列

主队列

  • 专门用来在主线程上调度任务的串行队列
  • 不会开启线程
  • 如果当前主线程正在有任务执行,那么无论主队列中当前被添加了什么任务,都不会被调度
  • dispatch get main queue();

全局队列

  • 为了方便程序员的使用,苹果提供了全局队列dispatch_get_global queue(0, 0)
  • 全局队列是一个并发队列
  • 在使用多线程开发时,如果对队列没有特殊需求,在执行异步任务时,可以直接使用全局队列

三、GCD相关面试题

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

这个程序就是典型的死锁,可以看到,只打印了1,就再也没有响应了,已经造成了GCD死锁。为什么会这样呢?让我们来解读一下这段程序的运行顺序:首先会打印1,然后将主队列和一个block传入GCD同步函数dispatch_sync中,等待sync函数执行,直到它返回,才会执行打印3的语句。可是,竟然没有反应了?block中的2没有被打印出来,viewDidLoad()中的3也没有被打印出来。也就是说,block没有得到执行的机会,viewDidLoad也没有继续执行下去。为什么block不执行呢?因为viewDidLoad也是执行在主队列的,它是正在被执行的任务,也就是说,viewDidLoad()是主队列的队头。主队列是串行队列,任务不能并发执行,同时只能有一个任务在执行,也就是队头的任务才能被出列执行。我们现在被执行的任务是viewDidLoad(),然后我们又将block入列到同一个队列,它比viewDidLoad()后入列,遵循先进先出的原理,它必须等到viewDidLoad()执行完,才能被执行。 但是,dispatch_sync函数的特性是,等待block被执行完毕,才会返回,因此,只要block一天不被执行,它就一天不返回。我们知道,内部方法不返回,外部方法是不会执行下一行命令的。不等到sync函数返回,viewDidLoad打死也不会执行print End的语句,因此,viewDidLoad()一直没有执行完毕。block在等待着viewDidLoad()执行完毕,它才能上,sync函数在等待着block执行完毕,它才能返回,viewDidLoad()在等待着sync函数返回,它才能执行完毕。这样的三方循环等待关系,就造成了死锁。

参考:理解GCD死锁
关于GCD的例题:iOS面试题:阿里-P6一面-参考思路

总结:同步串行必阻塞当前队列(不一定会死锁)


  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);这里会导致打印5会后执行
    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均在同一并发队列中且打印1、2为异步函数,所以1、2、3的打印顺序不是确定的。
  • 重点在于打印3的同步函数,由于其在main串行队列中根据FIFO,会先执行完成打印3然后再执行打印0(同步函数会阻塞当前队列)
  • 执行打印0后再将打印7、8、9任务添加到并发队列中。所以3一定在0之前,7、8、9一定在0之后,1、2位置不确定。

答案:AC


四、GCD底层分析

1、寻找GCD底层库

添加dispatch_sync符号断点

运行代码:

所以GCD的底层源码在libdispatch.dylib库中

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

2、队列是如何创建的?串行队列和并发队列创建时如何区分的?

队列创建方法: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 创建:根据传入的串行/并发参数创建
    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;
    // 确定串行队列/并发队列的类名
    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指向的类名
    //根据qai.dqai_concurrent区分是串行队列还是并发队列
    _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的创建方法中我们可以发现只要队列创建传入的参数不是DISPATCH_QUEUE_CONCURRENT那就创建并发队列

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的创建过程我们可以看到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-全局并发队列宽度:4096-1
DISPATCH_QUEUE_WIDTH_MAX- 一般并发队列宽度: 4096-2


3、异步函数底层分析: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;

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

dispatch_async方法中主要调用两个方法:
_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;//调用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;// 保存调用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调用的堆栈:

因此的确是_dispatch_call_block_and_release方法调用了Block

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335