iOS底层学习——GCD底层源码分析(栅栏函数、信号量、调度组、事件源)

1,943 阅读18分钟

前两篇文章学习分析了,GCD队列和函数的使用方式、串行队列和并发队列的创建、同步函数和异步函数底层执行流程、串行队列的死锁、GCD单例的实现流程等。本篇继续对GCD的相关内容进行分析,如dispatch_barrier栅栏函数dispatch_semaphore信号量dispatch_group调度组dispatch_source事件源等,将从使用和底层原理两个角度去分析这些内容。

1.栅栏函数

栅栏函数最直接的作用是控制任务执⾏顺序,达到同步的效果。

系统提供了两个函数:

  • dispatch_barrier_async

    Submits a barrier block for asynchronous execution and returns immediately.(提交一个屏障块以在调度队列上异步执行。)

  • dispatch_barrier_sync

    Submits a barrier block for synchronous execution on a dispatch queue.(提交一个屏障块以在调度队列上同步执行。)

dispatch_barrier_syncdispatch_barrier_async的区别也就在于会不会阻塞当前线程,同时需要注意的是,栅栏函数只能控制同一并发队列。

1.栅栏函数的使用

  • 引入一个案例

    自定义了一个并发队列,并且添加3个异步函数,加下面代码:

    - (void)demo{
        dispatch_queue_t concurrentQueue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
    
        /* 1.异步函数 */
        dispatch_async(concurrentQueue, ^{
            NSLog(@"1");
        });
    
        /* 2. 异步函数 */
        dispatch_async(concurrentQueue, ^{
            sleep(0.5);
            NSLog(@"2");
        });
    
    //    // 栅栏函数
    //    dispatch_barrier_async(concurrentQueue, ^{
    //        NSLog(@"----%@-----", [NSThread currentThread]);
    //    });
    
        /* 3. 异步函数 */
        dispatch_async(concurrentQueue, ^{
            NSLog(@"3");
        });
    
        // 4
        NSLog(@"4");
    }
    

    运行结果还是很明确,因为该队列是一个并发队列,并且是异步函数,所以任务1任务2任务3任务4的执行顺序是混乱的。见下面运行结果:

    image.png

  • 添加栅栏函数dispatch_barrier_async

    现在有业务需求,确保任务1任务2先执行,才能执行任务3,可以添加一个栅栏函数,见下面代码:

    image.png

    添加一个栅栏函数dispatch_barrier_async,运行发现,该并发队列中的任务1任务2一定会先于栅栏函数运行,在栅栏函数运行之后,才会运行任务3。因为任务4是在主队列,所以并不影响任务4的正常执行。

  • 添加栅栏函数dispatch_barrier_sync

    依然是上面的案例,将栅栏函数改成dispatch_barrier_sync,运行效果又是怎样呢?

    image.png

    运行结果发现,依然满足业务需求,即任务1任务2一定会先于栅栏函数运行,在栅栏函数运行之后,才会运行任务3。同时dispatch_barrier_sync还有另外一个特点,会堵塞当前的线程,所以任务4会在栅栏函数执行后才会被执行。

  • 注意事项

    • 栅栏函数和其他的任务必须在同一个队列中
    • 不能使用全局并发队列

2.栅栏函数的底层原理

对于栅栏函数,我们知道可以起到同步的作用,同时全局并发队列不能使用,带着这两点,我们来分析源码,是不是这样。

我们以同步自定义并发队列为例,进行跟踪。

根据dispatch_barrier_synclibdispatch.dylib源码中全局搜索,栅栏函数的实现。一路跟踪,最终会找到_dispatch_barrier_sync_f_inline方法,这里跟踪流程不在描述。_dispatch_barrier_sync_f_inline方法见下图:

image.png

下面会走到哪里呢?添加_dispatch_sync_f_slow符号断点,成功进入到该方法,见下图:

image.png

这个方法已经很熟悉了,在分析同步函数执行流程和死锁的时候都分析过该方法,同时在调用这个方法时设置了DC_FLAG_BARRIER的标签。_dispatch_sync_f_slow方法见下图:

image.png

继续跟踪流程,再添加_dispatch_sync_invoke_and_complete_recurse的符号断点,并成功走到这里。见下图:

image.png

通过上面的运行堆栈,发现其流程为:_dispatch_sync_f_slow -> _dispatch_sync_invoke_and_complete_recurse -> _dispatch_sync_complete_recurse,最终定位到_dispatch_sync_complete_recurse方法,见下图:

image.png

思考一下,栅栏函数的作用是起到同步,也就是说队列中之前的任务没有执行完,栅栏函数肯定是不会走的。所以在进行栅栏函数调用之前,肯定是要进行递归处理,完成队列中的任务。带着这样的思维我们看源码,是不是这样的逻辑。

_dispatch_sync_complete_recurse方法中,进行了递归处理,如果当前存在barrier,则会将当前队列中的任务全部唤醒执行,调用dx_wakeup。唤醒执行完毕后,才会执行_dispatch_lane_non_barrier_complete,即当前队列任务已经执行完成了,并且没有栅栏函数,执行下面的流程。

要想走到下面的流程,栅栏函数要先移除,那么栅栏函数在哪里被执行或者被移除的呢?跟踪dx_wakeup执行流程。dx_wakeup是通过宏定义的函数,全局搜索并找到了定义的位置,见下图:

image.png

此源码在同步函数流程分析时,已经跟踪过。底层为不同类型的队列提供不同的调用入口。同样我们还有个问题没有解决,为什么全局并发队列不能用?我们分别分析自定义并发队列全局并发队列来。

  • 自定义并发队列

    自定义并发队列会调用_dispatch_lane_wakeup方法,定位源码,见下图:

    image.png

    首先会判断是否为barrier形式的,如果是,则会调用_dispatch_lane_barrier_complete方法,处理有栅栏函数的流程;如果没有,则走正常的并发队列流程,调用_dispatch_queue_wakeup方法。

    _dispatch_lane_barrier_complete,查看处理流程,见下图:

    image.png

    如果是串行队列,则会进行等待,直到其他的任务执行完成,按顺序执行;如果是并发队列,则会调用_dispatch_lane_drain_non_barriers将栅栏之前的任务执行完成。最终调用_dispatch_lane_class_barrier_complete方法,完成栅栏的清除,从而执行栅栏之后的任务的执行。

  • 全局并发队列

    如果是全局并发队列,dx_wakeup方法对应的是_dispatch_root_queue_wakeup方法,查看_dispatch_root_queue_wakeup源码实现,见下图:

    image.png

    在全局并发队列流程中,并没有栅栏函数的相关处理流程,也就是按照正常的并发队列来处理。

    全局并发队列为什么没有对栅栏函数进行处理呢?因为全局并发队列除了被我们使用,系统也在使用,如果添加了栅栏函数,会导致队列运行的阻塞,从而影响系统级的运行,所以栅栏函数也就不适用于全局并发队列

2.信号量

GCD中的信号量是指Dispatch Semaphore,是持有计数的信号。Dispatch Semaphore提供了三个函数。

  • dispatch_semaphore_create:创建一个Semaphore并初始化信号的总量
  • dispatch_semaphore_wait:可以使总信号量减1,当信号总量为0时就会一直等待(阻塞所在线程),否则就可以正常执行
  • dispatch_semaphore_signal:发送一个信号,让信号总量加1,解锁

查看dispatch_semaphore_create的官方说明,见下图:

image.png

我们可以得出结论,信号量如果大于0,表示可以控制GCD的最大并发数。

1.信号量的使用

  • 案例1

    引入下面的案例,见下图代码:

        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        dispatch_semaphore_t sem = dispatch_semaphore_create(1);
    
        //任务1
        dispatch_async(queue, ^{
            dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); // 等待
            sleep(2);
            NSLog(@"执行任务1");
            NSLog(@"任务1完成");
            dispatch_semaphore_signal(sem); // 发信号
        });
    
        // 任务2
        dispatch_async(queue, ^{
            dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); // 等待、
    
            sleep(2);
            NSLog(@"执行任务2");
            NSLog(@"任务2完成");
            dispatch_semaphore_signal(sem); // 发信号
        });
    

    这也是我们通常的使用方式,在全局并发队列中,异步执行相关的任务,当前Semaphore的初始值为1,也就是说当前队列最大并发数为1dispatch_semaphore_wait表示阻塞,或者说占用一个信号,dispatch_semaphore_signal表示释放,也就是释放所占用的信号。

  • 案例2

    对上面的案例进行一些调整,我们将信号量初始值变为0,也就是最大并发数设置为0。异步并发执行两个任务,并且任务延迟了2秒钟,见下面代码:

    image.png

    正常理解,应该是先执行任务1,在执行任务2,但是实际的情况相反。这里dispatch_semaphore_wait有加锁的作用,而dispatch_semaphore_signal有解锁作用。当执行任务1时,dispatch_semaphore_wait加锁进行等待,当任务2执行完毕后,dispatch_semaphore_signal解锁发出信号,其他的任务可以执行,起到控制流程的作用。

  • 案例3

    信号量初始值变为0,也就是最大并发数设置为0dispatch_semaphore_wait在主线程中,异步流程中停顿2秒钟,正常情况下应该会先执行打印操作,number输出等于0才对,但是实际的情况是number等于1。见下图代码:

    image.png

    原因和案例2是一致的,dispatch_semaphore_wait加锁阻塞了当前线程,dispatch_semaphore_signal解锁后当前线程继续执行,number输出结果为1

2.信号量原理分析

dispatch_semaphore_waitdispatch_semaphore_signal加锁和解锁功能是如何实现的呢?带着这个问题,我们探索源码。

  • dispatch_semaphore_wait原理

    实现源码见下图:

    image.png

    os_atomic_dec2o进行减操作,也就是对创建是传入的value值进行减操作。以此来控制可并发数。例如:如果可并发数为3,则调用该方法后,变为2,表示占用一个并发数,剩下还可同时执行2个任务。但是,如果初始值是0,减操作之后为负数,则会调动_dispatch_semaphore_wait_slow方法。_dispatch_semaphore_wait_slow方法源码实现见下图:

    image.png

    该走哪个分支呢?上面的案例中我们调用dispatch_semaphore_wait时,传入的flagDISPATCH_TIME_FOREVER,表示一直等待。进入_dispatch_sema4_wait实现流程,见下图:

    image.png

    _dispatch_sema4_wait进行do-while循环,当不满足条件时,会一直循环下去,从而导致流程的阻塞。这也就解释了上面案例2案例3的执行结果。

  • dispatch_semaphore_signal原理

    实现源码如下:

    image.png

    os_atomic_inc2o是加操作,也就是对可用并发数据进行释放,将dispatch_semaphore_wait获取的一个执行权限释放掉。当信号量初始值是0时,调用加操作后,value值大于0,这样就可以获得执行权限。但是如果加一次后依然小于0,则会报异常:Unbalanced call to dispatch_semaphore_signal()。并调用_dispatch_semaphore_signal_slow方法的,这个方法做了什么呢?见下图:

    image.png

    这里_dispatch_sema4_signal同样会开启一个do-while循环,直到满足条件可以运行为止。

  • 总结Dispatch Semaphore在实际开发中主要作用:

    • 保持线程同步,将异步执行任务转换为同步执行任务
    • 保证线程安全,为线程加锁

3.调度组

dispatch_group,主要作用是控制任务的执行顺序。提供了以下方法:

  • dispatch_group_create 创建组
  • dispatch_group_async 进组任务并执行
  • dispatch_group_notify 进组任务执行完毕通知
  • dispatch_group_wait 进组任务执行等待时间
  • dispatch_group_enter 进组
  • dispatch_group_leave 出租

dispatch_group_enterdispatch_group_leave需要搭配起来使用。

1.调度组的使用

  • 调用组案例

    引入一个案例,有一个业务需求,要求完成任务1任务2任务3之后才能执行任务4。使用调度组可以采用以下方式:

    image.png

    把各个queue加到group里,然后当组中任务完成后再调用任务4,这里使用了dispatch_group_wait进行等待。dispatch_group_wait()函数会一直等到前面group中的内容执行完再执行下面内容,但会产生阻塞线程的问题。这也就导致了主线程中的任务5不能正常运行,直到任务组的任务完成才能被调用。

  • dispatch_group_notify的使用

    为解决上面的问题,可采用dispatch_group_notify进行任务执行完毕的通知,见下图:

    image.png

    采用这种方式后,任务5不会被阻塞,当任务组中的任务执行完毕后,再通知任务4执行。

  • 进组出组的使用

    dispatch_group_enterdispatch_group_leave搭配使用也可以完成上面的效果,见下图:

    image.png

    需要注意的是,使用这种方法,一个enter必须对应一个leave,成对出现!当所有任务都执行完成并出组后,才会执行任务4,并且不会阻塞任务5的执行。

    如果enterleave没有成对出现,比如多了一个leave则会崩溃,见下图:

    image.png

    如果多一个进组enter,则后续的任务则不能正常运行。见下图:

    image.png

2.调度组原理分析

这里思考一个问题,dispatch_group_enter进组和dispatch_group_leave出组为什么能够起到与调度组dispatch_group_async一样的效果呢?

  • dispatch_group_create

    我们先看看调度组的创建流程。dispatch_group_create方法实现见下图:

    image.png

    会调用_dispatch_group_create_with_count方法,并默认传入0_dispatch_group_create_with_count的实现见下图:

    image.png

    通过os_atomic_store2o进行保存。

  • dispatch_group_enter

    查看dispatch_group_enter实现源码,见下图:

    image.png

    os_atomic_sub_orig2o会进行--减减操作,此时的old_bits等于-1

  • dispatch_group_leave

    查看dispatch_group_leave实现源码,见下图:

    image.png

    这里通过os_atomic_add_orig2o++加加操作获取了old_state,此时old_state就等于0。而0&DISPATCH_GROUP_VALUE_MASK依然等于0,也就是old_value等于0。与此同时,DISPATCH_GROUP_VALUE_1的定义见下面代码:

        #define DISPATCH_GROUP_VALUE_MASK       0x00000000fffffffcULL
        #define DISPATCH_GROUP_VALUE_1          DISPATCH_GROUP_VALUE_MASK
    

    很显然old_value是不等于DISPATCH_GROUP_VALUE_MASK的,所以流程会进入到外层的if中,并调用_dispatch_group_wake方法进行唤醒,唤醒谁呢?唤醒的就是dispatch_group_notify方法,也就是说,如果不调用dispatch_group_leave方法,也就不会唤醒dispatch_group_notify,下面的流程也就不会执行。

  • dispatch_group_notify

    查看dispatch_group_notify源码发现,在old_state等于0的情况下,才会去唤醒相关的同步异步函数执行流程。见下图:

    image.png

    dispatch_group_leave分析中,我们已经得到old_state结果等于0

    所以这里也就解释了dispatch_group_enterdispatch_group_leave为什么要配合起来使用的原因,通过信号量的控制,避免异步的影响,能够及时唤醒并调用dispatch_group_notify方法。

  • dispatch_group_async的封装

    另外一个问题,为什么说dispatch_group_async就等于dispatch_group_enterdispatch_group_leave呢?一起探究一下dispatch_group_async封装。

    同样的思路,在libdispatch.dylib源码中搜索dispatch_group_async的定义,见下图:

    image.png

    调用了_dispatch_continuation_group_async方法,查看其实现:

    image.png

    在该方法中,可以发现,在调用dispatch_group_async方法向组中添加任务时,就调用了dispatch_group_enter方法,将信号量0变成了-1

    那么如果需要将信号量重置,一定是在任务执行完毕后再调用dispatch_group_leave方法。继续跟踪代码,调用_dispatch_continuation_async方法,其源码实现见下图:

    image.png

    很熟悉啊!又回到了异步函数的流程了!具体异步函数分析过程见GCD函数和队列原理探索这里不再跟踪分析。

    异步函数最终会调用_dispatch_worker_thread2方法,GCD函数和队列原理探索中也分析过,通过查看堆栈可以查看其运行流程。见下图:

    image.png

    跟踪流程会调用_dispatch_continuation_pop_inline -> _dispatch_continuation_invoke_inline方法。_dispatch_continuation_invoke_inline实现见下图:

    image.png

    在该方法中会针对组的情况进行处理,调用_dispatch_continuation_with_group_invoke方法。见下图:

    image.png

    在这里完成_dispatch_client_callout函数调用后,紧接着调用dispatch_group_leave方法,将信号量由-1变成了0

至此完成闭环,完整的分析了调度组进组出组通知的底层原理和关系。

4.事件源

我们在日常开发中,经常会使用计时器NSTimer,但是NSTimer需要加入到NSRunloop中,还受到mode的影响。例如,如果选择的modedefault的话,当滑动scrollView的时候,模式切换,定时器就会停止(当然可以将NSTimer放到commonMode中)。与此同时,如果Runloop正在进行连续性的运行,timer就可能会被延迟。

GCD提供了一个解决方案dispatch_source源dispatch_source有哪些特性呢?

  • 时间较准确,CPU负荷小,占用资源少
  • 可以使用子线程,解决定时器跑在主线程上卡UI问题
  • 可以暂停,继续,不用像NSTimer一样需要重新创建

关键方法:

  • dispatch_source_create 创建源
  • dispatch_source_set_event_handler 设置源事件回调
  • dispatch_source_merge_data 源事件设置数据
  • dispatch_source_get_data 获取源事件数据
  • dispatch_resume 继续
  • dispatch_suspend 挂起

1.事件源的使用

  • 创建事件源

    // 方法声明
    dispatch_source_t dispatch_source_create(
            dispatch_source_type_t type,
            uintptr_t handle,
            unsigned long mask,
            dispatch_queue_t _Nullable queue);
    
    // 实现过程
    dispatch_source_t source =  dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0,  dispatch_get_main_queue());
    

    创建过程需要传入两个重要的参数:

    • dispatch_source_type_t 要创建的源类型
    • dispatch_queue_t 事件处理程序块将提交到的调度队列
  • 事件源类型

    • DISPATCH_SOURCE_TYPE_DATA_ADD        用于合并数据
    • DISPATCH_SOURCE_TYPE_DATA_OR        按位OR用于合并数据
    • DISPATCH_SOURCE_TYPE_DATA_REPLACE 新获得的数据值替换现有的
    • DISPATCH_SOURCE_TYPE_MACH_SEND       监视Mach端口的调度源,只有发送权,没有接收权
    • DISPATCH_SOURCE_TYPE_MACH_RECV 监视Mach端口的待处理消息
    • DISPATCH_SOURCE_TYPE_MEMORYPRESSURE  监控系统的变化,内存压力状况
    • DISPATCH_SOURCE_TYPE_PROC            监视外部进程的事件的调度源
    • DISPATCH_SOURCE_TYPE_READ 监控文件描述符的调度源可供读取的字节
    • DISPATCH_SOURCE_TYPE_SIGNAL          用于监视当前进程的信号
    • DISPATCH_SOURCE_TYPE_TIMER 基于计时器的调度源
    • DISPATCH_SOURCE_TYPE_VNODE 监视事件文件描述符的调度源
    • DISPATCH_SOURCE_TYPE_WRITE          监视事件,写入字节的缓冲区空间
  • 计时器案例

    使用dispatch_source设计一个计时器,1秒钟执行一次,能够暂停、开始,同时不受主线程影响。见下图实现代码:

    @interface ViewController ()
    @property (nonatomic, strong) dispatch_source_t source;
    @property (nonatomic, strong) dispatch_queue_t queue;
    @property (nonatomic, assign) NSUInteger souceComplete;
    @property (nonatomic) BOOL isRunning;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.souceComplete = 0;
        
        // 开始时间
        dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, 3.0 * NSEC_PER_SEC);
        // 间隔时间
        uint64_t interval = 1.0 * NSEC_PER_SEC;
        
        // source
        self.queue = dispatch_queue_create("test", NULL);
        self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
        
        // 设置计时器
        dispatch_source_set_timer(self.source, start, interval, 0);
    
        __weak __typeof(self) weakSelf = self;
        dispatch_source_set_event_handler(self.source, ^{
            NSLog(@"source --- %lu    ------  %@", (unsigned long)weakSelf.souceComplete++, [NSThread currentThread]);
        });
    
        // 默认启动
        self.isRunning = YES;
        dispatch_resume(self.source);
    }
    
    // 计时器控制
    - (IBAction)didClickStartOrPauseAction:(id)sender {
        if (self.isRunning) {
            dispatch_suspend(self.source);
            dispatch_suspend(self.queue);
            self.isRunning = NO;
            [sender setTitle:@"暂停中.." forState:UIControlStateNormal];
        }else{
            dispatch_resume(self.source);
            dispatch_resume(self.queue);
            self.isRunning = YES;
            [sender setTitle:@"计时中.." forState:UIControlStateNormal];
        }
    }
    
    @end
    

    image.png

  • 注意事项

    1. Dispatch Source Timer是间隔定时器,也就是说每隔一段时间间隔定时器就会触发。在NSTimer中要做到同样的效果需要手动把repeats设置为 YES
    2. dispatch_source_set_timer中第二个参数,当我们使用dispatch_time或者DISPATCH_TIME_NOW时,系统会使用默认时钟来进行计时。然而当系统休眠的时候,默认时钟是不走的,也就会导致计时器停止。使用dispatch_walltime可以让计时器按照真实时间间隔进行计时。
    3. dispatch_source_set_timer的第四个参数leeway指的是一个期望的容忍时间,将它设置为1秒,意味着系统有可能在定时器时间到达的前1秒或者后1秒才真正触发定时器。在调用时推荐设置一个合理的leeway值。需要注意,就算指定leeway值为0,系统也无法保证完全精确的触发时间,只是会尽可能满足这个需求。
    4. event handler block中的代码会在指定的queue中执行。当queue是后台线程的时候,dispatch timer相比NSTimer就好操作一些了。因为NSTimer是需要Runloop支持的,如果要在后台dispatch queue中使用,则需要手动添加Runloop。使用dispatch timer就简单很多了。
    5. dispatch_source_set_event_handler这个函数在执行完之后,block会立马执行一遍,后面隔一定时间间隔再执行一次。而NSTimer第一次执行是到计时器触发之后。这也是和NSTimer之间的一个显著区别。
  • 停止source

    停止Dispatch Source有两种方法,但是这两种方式在使用时有很大的区别:

    • dispatch_suspend
    • dispatch_source_cancel

    使用dispatch_suspend时,source本身的实例需要一直保持。dispatch_suspend之后的source,是不能被释放的,如果释放会崩溃,见下图: image.png

    使用dispatch_source_cancel则没有这个限制,dispatch_source_cancel是真正意义上的取消source。被取消之后如果想再次执行source,只能重新创建新的source。这个过程类似于对NSTimer执行invalidate。见下图: image.png

  • source挂起计数说明

    dispatch_suspend严格上只是把source暂时挂起,它和dispatch_resume是一个平衡调用,两者分别会减少和增加dispatch对象的挂起计数。当这个计数大于0的时候,source就会执行。在挂起期间,产生的事件会积累起来,等到dispatch_resume的时候会融合为一个事件发送。

    1. 重复启动一个正在执行的源会崩溃

      image.png

    2. 连续挂起,同样需要连续对应次数的启动才能够正常运行

      image.png

    dispatch source并没有提供用于检测source本身的挂起计数的API,也就是说外部不能得知一个source当前是不是挂起状态,在设计代码逻辑时需要考虑到这两点。

2.事件源原理分析

带着上面使用过程中遇到的问题,我们来分析底层实现。

  • 一个时间源正在运行,重复调用dispatch_resume会为什么会崩溃

    查找dispatch_resume的底层实现原理,见下图:

    image.png

    紧接着调用了_dispatch_lane_resume方法,该方法的源码,见下图:

    image.png

    通过解读源码发现,底层会对事件源的相关状态进行判断,如果其进行过度恢复,则会走到over_resume流程,直接调起DISPATCH_CLIENT_CRASH崩溃。

    同时这里还维护了挂起计数,挂起计数包含所有挂起和非活动位的挂起计数。下溢意味着需要过度恢复或暂停计数转移到边计数,也就是说如果当前计数器还没有到可运行的状态,需要连续恢复。

  • 连续挂起

    从上面的案例中我们发现,连续挂起后需要对应次数的恢复过程才能执行,那么底层肯定是维护了一个信号量。首先搜索dispatch_suspend的实现,见下图:

    image.png

    进入dispatch_suspend方法,会调动_dispatch_lane_suspend方法,进入_dispatch_lane_suspend的方法实现,见下图:

    image.png

    通过下符号断点,发现下面会进入_dispatch_lane_suspend_slow流程。见下图:

    image.png

    同样这里维护一个暂停计数,如果连续调用挂起方法,则会进行减法的无符号下溢。

至此GCD的相关内容基本分析完成,如有新的知识点,后期再补充。

# GCD函数和队列原理探索

# GCD同步异步函数、死锁、GCD单例