iOS之GCD底层探索

一、引言

前边一篇文章我们已经大致介绍了GCD的有些概念和函数的执行。接下来让我们继续带着探索的心里去学习,继续前行,继续介绍线程是如何开辟和创建的,又是什么时候去执行相关的调度任务。接下来让我们从相关的概念入手

二 、线程和任务

2.1 GCD的优势

  • 1 GCD 是苹果公司为多核的并行运算提出的解决方案
  • 2 GCD 会自动利用更多的CPU内核(比如双核、四核)
  • 3 GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
  • 4 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码

2.2 任务

  • 1 任务使用 block 封装
  • 2 任务的 block 没有参数也没有返回值

2.3 同步任务

  • 1 函数: dispatch_sync
  • 2 必须等待当前语句执行完毕,才会执行下一条语句
  • 3 不会开启线程
  • 4 在当前执行 block 的任务

2.4异步任务

  • 1 异步 dispatch_async
  • 2 不用等待当前语句执行完毕,就可以执行下一条语句
  • 3 会开启线程执行 block 的任务
  • 4 异步是多线程的代名词

2.5主队列

  • 1 函数:dispatch_get_main_queue();
  • 2 特殊的串行队列
  • 3专门用来在主线程上调度任务的队列
  • 4不会开启线程
  • 5 如果当前主线程正在执行任务,那么无论主队列中当前被添加了什么任务,都不会被调度

2.6.全局队列

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

2.7 队列和任务搭配

  • 1 同步串行队列:不开启新线程,串行执行任务;
  • 2 同步并发队列:不开启新线程,串行执行任务;
  • 3同步主队列:如果在主线程调用,造成死锁;如果在其他线程调用,不开启新线程,串行执行,在主线程串行执行任务;
  • 4 异步串行队列:开启新线程,串行执行任务;
  • 5 异步并发队列:开启新线程,并发执行任务;
  • 6 异步主队列:不开启新线程,在主线程串行执行任务
  • 7 死锁:

三、GCD 底层源码分析

3.1 dispatch_async(异步函数)在何时开辟线程

我们都知道主线程和同步线程不会开辟子线程,但是当我们用dispatch_async(异步函数)的时候,线程是如何开辟的呢?是何时执行的调度任务呢?这些都是我们在使用GCD的情况下很少去考虑的问题,为了弄清楚相关的原理,让我们详细去研究一下相关的流程。

我们在测试Demo中打印一个简单的语句,并且进行相应的断点调试,然后打印出相应的堆栈信息

    dispatch_async(_queue, ^{
        
        NSLog(@"1111111");
        
    })

在控制台进行bt打印所有的堆栈信息,可以得出相应的执行步骤如下

异步函数执行步骤.png

从打印结果看我们知道异步线程会执行_dispatch_lane_invoke,有兴趣的同学可以加一个符号断点进行调试,程序一定会执行到此为止的,我们进入相应的定义

void
_dispatch_lane_invoke(dispatch_lane_t dq, dispatch_invoke_context_t dic,
        dispatch_invoke_flags_t flags)
{
    _dispatch_queue_class_invoke(dq, dic, flags, 0, _dispatch_lane_invoke2);
}

然后到_dispatch_lane_serial_drain的定义

dispatch_queue_wakeup_target_t
_dispatch_lane_serial_drain(dispatch_lane_class_t dqu,
        dispatch_invoke_context_t dic, dispatch_invoke_flags_t flags,
        uint64_t *owned)
{
    flags &= ~(dispatch_invoke_flags_t)DISPATCH_INVOKE_REDIRECTING_DRAIN;
    return _dispatch_lane_drain(dqu._dl, dic, flags, owned, true);
}

最终会走到_dispatch_root_queue_poke_slow方法,进行相应的线程创建,
其中创建线程的代码如下

#if !defined(_WIN32)
    pthread_attr_t *attr = &pqc->dpq_thread_attr;
    pthread_t tid, *pthr = &tid;
#if DISPATCH_USE_MGR_THREAD && DISPATCH_USE_PTHREAD_ROOT_QUEUES
    if (unlikely(dq == &_dispatch_mgr_root_queue)) {
        pthr = _dispatch_mgr_root_queue_init();
    }
#endif
    do {
        _dispatch_retain(dq); // released in _dispatch_worker_thread
        while ((r = pthread_create(pthr, attr, _dispatch_worker_thread, dq))) {
            if (r != EAGAIN) {
                (void)dispatch_assume_zero(r);
            }
            _dispatch_temporary_resource_shortage();
        }
    } while (--remaining);
#else // defined(_WIN32)
#if DISPATCH_USE_MGR_THREAD && DISPATCH_USE_PTHREAD_ROOT_QUEUES
    if (unlikely(dq == &_dispatch_mgr_root_queue)) {
        _dispatch_mgr_root_queue_init();
    }

从最终的线程创建我们知道,线程创建过程当中,不断的判断需要创建多少线程,每创建一个就减少一个,从而调用pthread_create的API进行创建。这就是GCD线程创建的过程;

3.2 dispatch_barrier_async(栅栏函数)

dispatch_barrier_async(栅栏函数)他,通过其命名我们就知道是拦截的意思。也就是在栅栏函数之前的任务执行完成后,才能执行后边的任务。先看一下官方文档

dispatch_barrier_sync: Submits a barrier block object for execution and waits until that block completes.(提交一个栅栏函数在执行中,它会等待栅栏函数执行完)

dispatch_barrier_async: Submits a barrier block for asynchronous execution and returns immediately.(提交一个栅栏函数在异步执行中,它会立马返回)

作者理解:dispatch_barrier_sync 需要等待栅栏执行完才会执行栅栏后面的任务,而dispatch_barrier_async 无需等待栅栏执行完,会继续往下走(保留在队列里)

原因:在同步栅栏时栅栏函数在主线程中执行,而异步栅栏中开辟了子线程栅栏函数在子线程中执行

我们看看dispatch_barrier_async异步栅栏函数的执行结果

dispatch_queue_t concurrentQueue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(concurrentQueue, ^{
        sleep(1);
        NSLog(@"123");
    });
    /* 2. 栅栏函数 */ // - dispatch_barrier_sync
    dispatch_barrier_async(concurrentQueue, ^{
        NSLog(@"---------------------%@------------------------",[NSThread currentThread]);
    });
    /* 3. 异步函数 */
    dispatch_async(concurrentQueue, ^{
        NSLog(@"加载那么多,喘口气!!!");
    });
    NSLog(@"**********起来干!!");

执行结果是


异步栅栏函数的执行结果.png

我们再次看看dispatch_barrier_sync同步栅栏函数的执行结果:

  dispatch_queue_t concurrentQueue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(concurrentQueue, ^{
        sleep(1);
        NSLog(@"123");
    });
    /* 2. 栅栏函数 */ // - dispatch_barrier_sync
    dispatch_barrier_sync(concurrentQueue, ^{
        NSLog(@"---------------------%@------------------------",[NSThread currentThread]);
    });
    /* 3. 异步函数 */
    dispatch_async(concurrentQueue, ^{
        NSLog(@"加载那么多,喘口气!!!");
    });
    NSLog(@"**********起来干!!");
 

打印结果如下
同步栅栏函数的执行.png
结论 dispatch_barrier_sync 需要等待栅栏执行完才会执行栅栏后面的任务,而dispatch_barrier_async 无需等待栅栏执行完,会继续往下走(保留在队列里)

3.3 dispatch_semaphore_t(信号量)

信号量是通过信号的数量来控制相应的任务调度,需要设置一个固定的信号量,有可以操作的任务调度才可以执行,就好比停车场的空车位数一样,数量一定,如果有空就可以进行停车,如果车位已满就让后来的人等待。其中信号量控制有三个非常重要的函数

dispatch_semaphore_create
dispatch_semaphore_wait
dispatch_semaphore_signal
  • 1 dispatch_semaphore_create(long value);和GCD的group等用法一致,这个函数是创建一个dispatch_semaphore_类型的信号量,并且创建的时候需要指定信号量的大小。

  • 2 dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); 等待信号量。该函数会对信号量进行减1操作。如果减1后信号量小于0(即减1前信号量值为0),那么该函数就会一直等待,也就是不返回(相当于阻塞当前线程),直到该函数等待的信号量的值大于等于1,该函数会对信号量的值进行减1操作,然后返回。

  • 3 dispatch_semaphore_signal(dispatch_semaphore_t deem); 发送信号量。该函数会对信号量的值进行加1操作。

通常等待信号量和发送信号量的函数是成对出现的。并发执行任务时候,在当前任务执行之前,用dispatch_semaphore_wait函数进行等待(阻塞),直到上一个任务执行完毕后且通过dispatch_semaphore_signal函数发送信号量(使信号量的值加1),dispatch_semaphore_wait函数收到信号量之后判断信号量的值大于等于1,会再对信号量的值减1,然后当前任务可以执行,执行完毕当前任务后,再通过dispatch_semaphore_signal函数发送信号量(使信号量的值加1),通知执行下一个任务......如此一来,通过信号量,就达到了并发队列中的任务同步执行的要求。

dispatch_semaphore_signal底层实现过程

dispatch_semaphore_signal的内部实现是

dispatch_semaphore_signal(dispatch_semaphore_t dsema)
{
    long value = os_atomic_inc2o(dsema, dsema_value, release);
    if (likely(value > 0)) {
        return 0;
    }
    if (unlikely(value == LONG_MIN)) {
        DISPATCH_CLIENT_CRASH(value,
                "Unbalanced call to dispatch_semaphore_signal()");
    }
    return _dispatch_semaphore_signal_slow(dsema);
}

os_atomic_inc2o(dsema, dsema_value, release);起底层定义是

#define os_atomic_inc2o(p, f, m) \
        os_atomic_add2o(p, f, 1, m)

也就是调用了os_atomic_add2o的函数封装;其内部实现如下

#define os_atomic_add2o(p, f, v, m) \
        os_atomic_add(&(p)->f, (v), m)

也就是os_atomic_add 其内部定义封装是

#define os_atomic_add(p, v, m) \
        _os_atomic_c11_op((p), (v), m, add, +)

再次到_os_atomic_c11_op函数的实现,定义如下

图片.png

最终我们知道其内部是调用了atomic_fetch_##o##_explicit(_os_atomic_c11_atomic(p) 而其中##o##是形参,替换成第三个参数就是add得到的结果就是atomic_fetch_add_explicit,我们百度搜索就知道

原子替换指向的值obj和添加arg到旧值的结果obj,并返回obj先前保存的值。操作是读取 - 修改 - 写入操作。第一个版本根据命令对内存进行访问memory_order_seq_cst,第二个版本根据内存访问内存访问order 具体链接

同理,dispatch_semaphore_wait也是一样的原理过程。这就是信号量的底层实现逻辑。

3.4 dispatch_once_t(单利)的底层实现和逻辑

单利的底层实现代码定义如下


dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
{
    dispatch_once_gate_t l = (dispatch_once_gate_t)val;

#if !DISPATCH_ONCE_INLINE_FASTPATH || DISPATCH_ONCE_USE_QUIESCENT_COUNTER
    uintptr_t v = os_atomic_load(&l->dgo_once, acquire);
    if (likely(v == DLOCK_ONCE_DONE)) {
        return;
    }
#if DISPATCH_ONCE_USE_QUIESCENT_COUNTER
    if (likely(DISPATCH_ONCE_IS_GEN(v))) {
        return _dispatch_once_mark_done_if_quiesced(l, v);
    }
#endif
#endif
    if (_dispatch_once_gate_tryenter(l)) {
        return _dispatch_once_callout(l, ctxt, func);
    }
    return _dispatch_once_wait(l);
}
单利的保证线程安全过程

根据以上代码可知,

  • 1 我们可以知道外部传入的一个任务值val,单利内部会对这个任务值进行一个处理,处理成一个dispatch_once_gate_t对象l,
  • 2 通过内核加载过程,看看当前任务十分已经被标记过是需要执行的任务;
  • 3 如果是被标记过的任务就直接执行当前任务,并且对当前任务的执行流程加入一个锁操作;
  • 4 如果没有标记过,就对改任务进行标记,并且返回一个标识;
  • 5 最后通过CPU调度,选择已经标识过的任务进行执行_dispatch_once_callout
  • 6 同时有多个任务来就需要等待当前任务的处理,等到处理过后进行标识才能从新调用;

四、总结

以上就是GCD相关函数的底层实现过程以及一些原理的介绍,仅仅凭借个人的学习经验和代码阅读过程以及Demo的调试得到的感触,所以很多东西只是个人的理解,如果大神们有更好的思路和意见,欢迎多多指正。我一定虚心学习,追求大神们的步伐。

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