美文网首页多线程GCD
iOS GCD底层分析(3)--栅栏函数、信号量、调度组、事件源

iOS GCD底层分析(3)--栅栏函数、信号量、调度组、事件源

作者: 冼同学 | 来源:发表于2021-09-24 18:31 被阅读0次

前言

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

准备工作

1. 栅栏函数

1.1 常用的栅栏函数

  • dispatch_barrier_async
    前面的任务执行完毕才会执行barrier中的逻辑,以及barrier后加入队列的任务。
  • dispatch_barrier_sync
    作用相同,但是会堵塞线程,影响后面的任务执行。

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

1.2 栅栏函数的使用

自定义了一个并发队列,并且添加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的执行顺序是混乱的。见下面运行结果:

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

    添加dispatch_barrier_sync
    分析:
    任务1任务2一定会先于栅栏函数运行,在栅栏函数运行之后,才会运行任务3。同时dispatch_barrier_sync还有另外一个特点,会堵塞当前的线程,所以任务4会在栅栏函数执行后才会被执行
  • 添加栅栏函数dispatch_barrier_async

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

    • dispatch_barrier_sync会阻塞当前线程
    • 栅栏函数和其他的任务必须在同一个队列中
    • 不能使用全局并发队列(后面会分析)

1.3 栅栏函数的底层原理

我们对栅栏函数的任务无非就是栅栏函数起到同步的作用全局并发队列不能够执行栅栏函数。那我们分析一下源码,看看源码是怎么样的逻辑,请往下走。
libdispatch.dylib源码中全局搜索dispatch_barrier_sync,一路往下跟踪最终找到了_dispatch_barrier_sync_f_inline方法中,如下图:

_dispatch_barrier_sync_f_inline
通过下符号断点_dispatch_sync_f_slow,成功进入了该方法,说明栅栏函数是进入以上判断的,如下图:
下_dispatch_sync_f_slow符号断点
_dispatch_sync_f_slow方法在之前同步函数执行死锁时候已经分析过,同时在调用这个方法时设置了DC_FLAG_BARRIER的标签。_dispatch_sync_f_slow方法见下图:
_dispatch_sync_f_slow
因为func基本不会为NULL,那我们添加_dispatch_sync_invoke_and_complete_recurse符号断点,发现的确进入了这个方法,如下:
下_dispatch_sync_invoke_and_complete_recurse符号断点
通过上面的运行堆栈,发现其流程为:_dispatch_sync_f_slow -> _dispatch_sync_invoke_and_complete_recurse -> _dispatch_sync_complete_recurse,最终定位到_dispatch_sync_complete_recurse方法,见下图:
_dispatch_sync_complete_recurse
分析:
栅栏函数的作用是起到同步,也就是说队列中之前的任务没有执行完,栅栏函数肯定是不会走的。所以在进行栅栏函数调用之前,肯定是要进行递归处理,完成队列中的任务
_dispatch_sync_complete_recurse方法中,进行了递归处理,如果当前存在barrier,则会将当前队列中的任务全部唤醒执行,调用dx_wakeup。唤醒执行完毕后,才会执行_dispatch_lane_non_barrier_complete,即当前队列任务已经执行完成了,并且没有栅栏函数,执行下面的流程。

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

dx_wakeup
之前我们已经说过,底层为不同类型的队列提供不同的调用入口那为什么全局并发队列不能够用栅栏函数呢?继续往下看!

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

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

进入_dispatch_lane_barrier_complete方法,查看流程,如下:

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

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

_dispatch_root_queue_wakeup
在全局并发队列流程中,并没有栅栏函数的相关处理流程,也就是按照正常的并发队列来处理
总结:
全局并发队列为什么没有对栅栏函数进行处理呢?因为全局并发队列除了被我们使用,系统也在使用,如果添加了栅栏函数,会导致队列运行的阻塞,从而影响系统级的运行,所以栅栏函数也就不适用于全局并发队列

2. 信号量

在使用GCD过程中我们也会用到信号量(Dispatch Semaphore),持有计数的信号。Dispatch Semaphore提供了三个函数。

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

查看dispatch_semaphore_createAPI相关说明如下:

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

2.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秒钟,见下面代码:

    案例2
    如果没有加入信号量的话,一般情况都会先执行任务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。见下图代码:

    案例3

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

2.2 信号量原理分析

我们探究原理肯定是带着目的去的,那么我们以下就是主要探索dispatch_semaphore_waitdispatch_semaphore_signal加锁和解锁功能是如何实现的,跟着走吧。

2.2.1 dispatch_semaphore_wait原理

libdispatch.dyld中查找其实现源码如下:

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

_dispatch_semaphore_wait_slow实现如下:

_dispatch_semaphore_wait_slow
上面的案例中我们调用dispatch_semaphore_wait时,传入的flagDISPATCH_TIME_FOREVER,表示一直等待。进入_dispatch_sema4_wait实现流程,如下图:
_dispatch_sema4_wait
分析:
由上图看出_dispatch_sema4_wait的实现是在lock(锁)的相关文件,可以知道_dispatch_sema4_wait是对锁进行操作的。
_dispatch_sema4_wait进行do-while循环,当不满足条件时,会一直循环下去,从而导致流程的阻塞。这也就解释了上面案例2案例3的执行结果。
2.2.2 dispatch_semaphore_signal原理

其实现代码如下:

dispatch_semaphore_signal
os_atomic_inc2o加操作,也就是对可用并发数据进行释放,将dispatch_semaphore_wait获取的一个执行权限释放掉
当信号量初始值是0时,调用加操作后,value值大于0,这样就可以获得执行权限。但是如果加一次后依然小于0,则会报异常:Unbalanced call to dispatch_semaphore_signal()。并调用_dispatch_semaphore_signal_slow方法。
_dispatch_semaphore_signal_slow实现如下:
_dispatch_semaphore_signal_slow
_dispatch_sema4_signal同样会开启一个do-while循环,直到满足条件可以运行为止
_dispatch_sema4_signal

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必须要成对使用

3.1 调度组的使用

  • 调度组案例
    要求完成任务1任务2任务3之后才能执行任务4。使用调度组可以采用以下方式:

    调度组案例
    各个queue加到group里,然后当组中任务完成后再调用任务4,这里使用了dispatch_group_wait进行等待。dispatch_group_wait()函数会一直等到前面group中的内容执行完再执行下面内容,但会产生阻塞线程的问题。这也就导致了主线程中的任务5不能正常运行,直到任务组的任务完成才能被调用。
  • dispatch_group_notify的使用
    为解决上面的问题,可采用dispatch_group_notify进行任务执行完毕的通知,见下图:

    dispatch_group_notify的使用
    采用这种方式后,任务5不会被阻塞,当任务组中的任务执行完毕后,再通知任务4执行。
  • 进组出组的使用
    dispatch_group_enterdispatch_group_leave搭配使用也可以完成上面的效果,见下图:

    进出组的使用
    一个enter必须对应一个leave成对出现!当所有任务都执行完成并出组后,才会执行任务4,并且不会阻塞任务5的执行

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

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

3.2 调度组底层原理分析

dispatch_group_enter进组和dispatch_group_leave出组为什么能够起到与调度组dispatch_group_async一样的效果呢?

  • dispatch_group_create
    dispatch_group_create方法实现见下图:

    dispatch_group_create
    会调用_dispatch_group_create_with_count方法,并默认传入0_dispatch_group_create_with_count的实现见下图:
    _dispatch_group_create_with_count
    通过os_atomic_store2o进行保存。
  • dispatch_group_enter
    查看dispatch_group_enter实现源码,见下图:

    dispatch_group_enter
    os_atomic_sub_orig2o会进行--减减操作,此时的old_bits等于-1
  • ** dispatch_group_leave**
    查看dispatch_group_leave实现源码,见下图:

    dispatch_group_leave
    这里通过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
#define DISPATCH_GROUP_VALUE_MASK       0x00000000fffffffcULL

很显然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的情况下,才会去唤醒相关的同步异步函数执行流程。见下图:

    dispatch_group_notify
    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封装。
    dispatch_group_async的定义,见下图:

    dispatch_group_async
    进入_dispatch_continuation_group_async方法如下:
    _dispatch_continuation_group_async
    在调用dispatch_group_async方法向组中添加任务时,就调用了dispatch_group_enter方法,将信号量0变成了-1
    那么如果需要将信号量重置,一定是在任务执行完毕后再调用dispatch_group_leave方法。继续跟踪代码,调用_dispatch_continuation_async方法,其源码实现见下图:
    _dispatch_continuation_async
    又回到了异步函数的流程了!具体异步函数分析过程见iOS GCD底层分析(1),这里不再跟踪分析。

异步函数最终会调用_dispatch_worker_thread2方法,那么我们查看堆栈信息得到如下:

队列组堆栈信息
跟踪流程会调用_dispatch_continuation_pop_inline -> _dispatch_continuation_invoke_inline方法。

先进入_dispatch_root_queue_drain方法,如下:

_dispatch_root_queue_drain
跟踪进入到_dispatch_continuation_pop_inline方法,如下:
_dispatch_continuation_pop_inline
跟踪进入到_dispatch_continuation_invoke_inline方法,如下:
_dispatch_continuation_invoke_inline
跟踪进去_dispatch_continuation_with_group_invoke方法,如下:
_dispatch_continuation_with_group_invoke
在这里完成_dispatch_client_callout函数调用后,紧接着调用dispatch_group_leave方法,将信号量由-1变成了0

注意:到此已经完整的分析了调度组进组出组通知的底层原理和关系。

4. 事件源

在日常的开过程中,我们经常会用到NSTimerNSTimer需要加入到NSRunloop中,还受到mode的影响。在mode设置不对的情况下,scrollView滑动的时候NSTimer也会收到影响。如果Runloop正在进行连续性的运行,timer就可能会被延迟

GCD提供了一个解决方案dispatch_source源。dispatch_source有以下几种特性:

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

dispatch_source源的关键方法:

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

4.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
  • 运行案例

    事件源案例
  • 注意事项:

  • Dispatch Source Timer间隔定时器,也就是说每隔一段时间间隔定时器就会触发。在NSTimer中要做到同样的效果需要手动把repeats设置为 YES

  • dispatch_source_set_timer中第二个参数,当我们使用dispatch_time或者DISPATCH_TIME_NOW时,系统会使用默认时钟来进行计时。然而当系统休眠的时候,默认时钟是不走的,也就会导致计时器停止。使用dispatch_walltime可以让计时器按照真实时间间隔进行计时。

  • dispatch_source_set_timer的第四个参数leeway指的是一个期望的容忍时间,将它设置为1秒,意味着系统有可能在定时器时间到达的前1秒或者后1秒才真正触发定时器。在调用时推荐设置一个合理的leeway值。需要注意,就算指定leeway值为0,系统也无法保证完全精确的触发时间,只是会尽可能满足这个需求

  • event handler block中的代码会在指定的queue中执行。当queue是后台线程的时候,dispatch timer相比NSTimer就好操作一些了。因为NSTimer是需要Runloop支持的,如果要在后台dispatch queue中使用,则需要手动添加Runloop。使用dispatch timer就简单很多了。

  • dispatch_source_set_event_handler这个函数在执行完之后,block会立马执行一遍,后面隔一定时间间隔再执行一次。而NSTimer第一次执行是到计时器触发之后。这也是和NSTimer之间的一个显著区别。

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

    • dispatch_suspend
    • dispatch_source_cancel

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

释放source案例
使用dispatch_source_cancel则没有这个限制,dispatch_source_cancel是真正意义上的取消source。被取消之后如果想再次执行source,只能重新创建新的source。这个过程类似于对NSTimer执行invalidate。见下图:
cance案例
  • source挂起计数说明
    dispatch_suspend严格上只是把source暂时挂起,它和dispatch_resume是一个平衡调用,两者分别会减少增加dispatch对象的挂起计数。当这个计数大于0的时候,source就会执行。在挂起期间,产生的事件会积累起来,等到dispatch_resume的时候会融合为一个事件发送
  1. 重复启动一个正在执行的源会崩溃


    重复执行源
  2. 连续挂起,同样需要连续对应次数的启动才能够正常运行
    连续挂起source
    注意:dispatch source并没有提供用于检测source本身的挂起计数的API,也就是说外部不能得知一个source当前是不是挂起状态,在设计代码逻辑时需要考虑到这两点。

4.2 事件源底层原理分析

通常我们分析原理都是带着问题触发的,那么这次我们探索根据以上的问题:为什么source在运行时,重复调用dispatch_resume方法就会崩溃?在以下我们看看底层原理就一清二楚了。

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

dispatch_resume
接着进去_dispatch_lane_resume方法查看源码,如下:
重复resume
重复resume直接进入到了over_resume方法里面,查看其实现如下:
over_resume
通过解读源码发现,底层会对事件源的相关状态进行判断,如果其进行过度恢复,则会走到over_resume流程,直接调起DISPATCH_CLIENT_CRASH崩溃。
同时这里还维护了挂起计数(old_state),挂起计数包含所有挂起和非活动位的挂起计数。下溢意味着需要过度恢复或暂停计数转移到边计数,也就是说如果当前计数器还没有到可运行的状态,需要连续恢复。
  • 连续挂起
    我们发现,连续挂起后需要对应次数的恢复过程才能执行,那么底层肯定是维护了一个信号量。首先搜索dispatch_suspend的实现,见下图:
    dispatch_suspend
    接着进去_dispatch_lane_suspend方法查看源码的实现,如下:
    _dispatch_lane_suspend
    通过下符号断点发现会进入_dispatch_lane_suspend_slow的流程,源码实现如下:
    _dispatch_lane_suspend_slow
    果不其然,同样这里维护一个暂停计数,如果连续调用挂起方法,则会进行减法的无符号下溢

总结

花了不少的时间,GCD的探索就到此结束了,过程好艰辛但是收获也是满满的。iOS的底层学习任重而道远,继续努力。

相关文章

网友评论

    本文标题:iOS GCD底层分析(3)--栅栏函数、信号量、调度组、事件源

    本文链接:https://www.haomeiwen.com/subject/ypbogltx.html