OC多线程学习(二) - GCD

本文内容:

  1. GCD相关概念
  2. 有关GCD的几道面试题
  3. 源码分析:队列和异步函数

GCD概念


GCD是Grand Central Dispatch的缩写。是苹果为提供多核并行运算而提出的解决方案。主要功能作用:将任务添加到队列,并且指定执行任务的函数。而且开发人员不需要编写管理线程生命周期的代码。

任务

GCD中的任务用block封装,并有以下特点:

  • 任务block没有参数也没有返回值
  • 不需要手动调用block,GCD内部帮我们调用

函数

GCD中的函数总体分为:同步函数dispatch_sync和异步函数dispatch_async

  • 同步函数dispatch_sync
    • 等待当前语句执行完毕
    • 不会开启线程
    • 在当前线程执行任务
  • 异步函数dispatch_async
    • 不用等待当前语句执行完毕
    • 会开启线程新线程执行任务

队列

队列是一种数据结构。具有先进先出的特性。GCD中大致分为两种队列类型,串行队列并发队列

根据调用不同的函数(同步or异步),会有以下四种情况:

--- 同步函数 异步函数
串行队列 1.不会开启线程 2.任务按顺序执行 3.会产生堵塞 1.开启新线程 2.任务按顺序执行
并发队列 1.不会开启线程 2.任务按顺序执行 1.开启新线程 2.任务异步执行,没有顺序,与CPU的调度有关

队列和线程的关系

面试的时候经常会被问到队列和线程之间的关系?
其实他们是没有太大的关系的,队列是一种数据结构,作为任务的容器。线程是进程的基本执行单元,是任务的执行者。CPU调度线程去执行容器中的任务。所以说队列和线程没有直接关系,只是在不同业务层级中担当不同的角色罢了。

GCD一些相关面试题


面试题1:
dispatch_queue_t queue = dispatch_queue_create("HelloGCD", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_async(queue, ^{
    NSLog(@"2");
    dispatch_async(queue, ^{
        NSLog(@"3");
    });
    NSLog(@"4");
});
NSLog(@"5");

答案:1、5、2、4、3
分析:队列是并行队列,两次调用异步函数(dispatch_async),都会开启新的线程执行任务,并且不会堵塞当前线程。

  1. 首先打印“1”,遇到异步函数不处理,然后打印“5”。
  2. 第一层异步函数内执行逻辑与外部类似,打印“2”和“4”。
  3. 最后执行第二次异步函数,打印“3”
面试题2
dispatch_queue_t queue = dispatch_queue_create("HelloGCD", DISPATCH_QUEUE_CONCURRENT);
    
dispatch_async(queue, ^{
    // sleep(2);
    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

答案:AC
分析:并行队列添加相关任务,其中“3”是同步函数,会堵塞(堵塞的代码行在同步函数代码行结束的位置,也就是当前代码NSLog(@"3");下一行的“});”)当前线程。“0”在主线程中执行,其他的都是异步函数,所以“0”后面的异步函数肯定都会在“0”之后执行。因此本题答案是“3”在“0”之前,并且“7”、“8”、“9”在“0”之后。因此答案是AC。注意:“1”和“2”的位置不确定,这个取决于任务的时间复杂度,可以打开“1”中的sleep,打印查看一下结果,“1”会在“9”之后打印。

面试题3:
// 串行队列
dispatch_queue_t queue = dispatch_queue_create("HelloGCD", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
// 异步函数
dispatch_async(queue, ^{
    NSLog(@"2");
    dispatch_sync(queue, ^{
        NSLog(@"3");
    });
    NSLog(@"4");
});
NSLog(@"5");

答案:1、5、2、崩溃(EXC_BAD_INSTRUCTION)
分析:

  1. 在主线程队列中(串行队列),依次加入“1”、异步函数(dispatch_async)代码块、“5”
  2. 异步函数开启子线程,不阻塞主线程,所以先打印“1”和“5”。
  3. 子线程中,由于是串行队列,所以会把“2”、同步函数dispatch_sync、“4”这三个“任务”依次加入到queue中。
  4. 子线程开始串行执行任务,打印“2”
  5. 队列下一个任务是同步函数,会阻塞当前队列,然后把“3”加入到队列中,此时会产生死锁,此时队列情况:==dispatch_sync的block - “4” - “3”==
    • 同步函数需要“3”执行完,自己才能执行结束。
    • 由于“3”是在“4”后面加入到队列,所以“3”要等待“4”执行完成。
    • “4”在同步函数后面加入到队列,所以得等待同步函数执行结束。
    • 等待情况:dispatch_sync - “3” - “4” - dispatch_sync,是互相等待的状态,因此出现了死锁。
面试题4
__block int a = 0;
while (a < 5) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        a++;
    });
}
    
NSLog(@"out a = %d", a);

问题1:最后的打印
A:0 B:<5 C:=5 D:>5
答案:CD
分析:
a初始化=0,进入while循环,循环条件是a<5,所以当a小于5的时候,都会在while循环中,所以可以排除A和B。循环中使用的是异步函数,异步函数会开辟线程,所以当其中一个线程的操作a++后满足跳出循环的条件了,就会退出循环,但是此时可能还会有其他线程还没有执行完,就会有a>=5的情况。因此答案是CD。

问题2:如何获取到循环中最后的a值
答案:在while循环外,使用相同的队列中,再次调用异步函数。

...

NSLog(@"out a = %d", a);
// add code 
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    sleep(1);
    NSLog(@"out a = %d", a);
});
// end add

分析:
在相同队列中,以相同的方式(异步函数)再追加一个任务,任务内容是打印a,为了队列中其他任务执行完毕,此处增加一个sleep,因为任务都比较简单(NSLog),就算不加也不会有太大的问题。
这个问题在正常开发中不会使用到,而且会浪费很大的性能(会有很多无用的线程执行无用的任务)。目的只是考餐对GCD队列的了解程度。

问题3:如何进行性能优化
答案:
用信号量加锁的方式

dispatch_semaphore_t s = dispatch_semaphore_create(1);
__block int a = 0;
while (a < 5) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"in a = %d, %@", a, [NSThread currentThread]);
        a++;
        dispatch_semaphore_signal(s);
    });
    dispatch_semaphore_wait(s, DISPATCH_TIME_FOREVER);
}
    
NSLog(@"out a = %d", a);

分析:
此处信号量wait方法,如果放在异步函数(dispatch_async)调用之前,那么异步函数的就没有使用的意义了(编程顺序执行,可以把异步函数的代码删掉了)。放在异步函数之后,异步函数还是有意义的,不懂的可以自己打印看看打印结果。信号量加锁的方式很容易理解,但是两个函数放的位置,还有根据具体的业务需求来自行决定。

底层分析

源码libdispatch下载地址

队列创建源码分析

队列也是对象,通过一个示例证明一下:


  • 代码中创建两个队列对象,一个是串行队列,另一个是并发队列
  • 通过runtime的api方法object_getClass,查看他们的归属类:
    • 串行队列:OS_dispatch_queue_serial
    • 并发队列:OS_dispatch_queue_concurrent
  • 接下来通过查看源码找到类名创建队列的地方,以及isa的指向
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);
}

static dispatch_queue_t
_dispatch_lane_create_with_target(const char *label, dispatch_queue_attr_t dqa,
        dispatch_queue_t tq, bool legacy)
{

    dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);
    
    ......
    
    const void *vtable;
    if (dqai.dqai_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
    _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
            
    ......
}
  • dispatch_queue_create函数有两个参数:
    • 第一个label:字符标示
    • 第二个attr:表示串行队列还是并发队列
  • 紧接着调用_dispatch_lane_create_with_target函数,前两个参数就是dispatch_queue_create的两个参数,第二个参数dqa就是attr
  • dqa封装成dispatch_queue_attr_info_t类型,变量是dqai,这里对传入的参数进行了判断,赋值给dqai.dqai_concurrent,然后DISPATCH_VTABLE宏获取不同队列的类名存入到vtable变量中
  • 后续调用_dispatch_object_alloc进行内存分配
void *
_dispatch_object_alloc(const void *vtable, size_t size)
{
#if OS_OBJECT_HAVE_OBJC1
//不关心的代码
    .....
#else
    return _os_object_alloc_realized(vtable, size);
#endif
}

inline _os_object_t
_os_object_alloc_realized(const void *cls, size_t size)
{
    _os_object_t obj;
    dispatch_assert(size >= sizeof(struct _os_object_s));
    while (unlikely(!(obj = calloc(1u, size)))) {
        _dispatch_temporary_resource_shortage();
    }
    obj->os_obj_isa = cls;//isa赋值
    return obj;
}
  • 调用_dispatch_object_alloc函数,间接调用_os_object_alloc_realized函数
  • _os_object_alloc_realized中看到了isa赋值代码:obj->os_obj_isa = cls;
  • 到此就我们就了解队列对象的整个初始化过程。

异步函数源码分析

主要的研究目标是任务block是如何被调用的。

dispatch_async(queue_c, ^{
    NSLog(@"12334");
});
  • dispatch_async有两个参数,第一个参数是队列,第二个是任务(block)
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;

    qos = _dispatch_continuation_init(dc, dq, work, 0, dc_flags);
    _dispatch_continuation_async(dq, dc, qos, dc->dc_flags);
}
  • work参数被传入到_dispatch_continuation_init函数中的第三个参数,其余的地方没有用到
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)
{
    //封装work成ctxt
    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);
    }

    //封装work成func
    dispatch_function_t func = _dispatch_Block_invoke(work);
    if (dc_flags & DC_FLAG_CONSUME) {
        func = _dispatch_call_block_and_release;
    }
    //ctxt和func作为参数传入
    return _dispatch_continuation_init_f(dc, dqu, ctxt, func, flags, dc_flags);
}
  • work被封装成ctxtfunc,然后传入到_dispatch_continuation_init_f函数中
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;//保存f
    dc->dc_ctxt = ctxt;//保存ctxt
    // 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的保存(dc->dc_ctxt = ctxt)和调用函数的保存(dc->dc_func = f),那么在什么时候调用呢?我们就需要查看调用堆栈了
  • 在任务block内下断点,然后bt命令查看调用栈。
void
_dispatch_client_callout(void *ctxt, dispatch_function_t f)
{
    @try {
        return f(ctxt);//之前保存的相关调用方法和任务
    }
    @catch (...) {
        objc_terminate();
    }
}

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