OC底层原理探索—GCD(下)—— 栅栏函数、调度组、信号量

栅栏函数

关于栅栏函数,系统提供了两个方法

  • dispatch_barrier_async
  • dispatch_barrier_sync
    dispatch_barrier_syncdispatch_barrier_async区别会不会阻塞当前的线程,要注意,栅栏函数只能控制同一队列。
    全局并发队列 :dispatch_get_global_queue会使栅栏函数失效

栅栏函数使用

同步栅栏函数
    
    dispatch_queue_t concurrentQueue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);

    /* 1.异步函数 */
    dispatch_async(concurrentQueue, ^{
        NSLog(@"1");
    });
    
    dispatch_async(concurrentQueue, ^{
        
        NSLog(@"2");
    });
    /* 2. 栅栏函数 */ 
    dispatch_barrier_sync(concurrentQueue, ^{
        NSLog(@"3");
    });
    /* 3. 异步函数 */
    dispatch_async(concurrentQueue, ^{
        NSLog(@"4");
    });
    // 4
    NSLog(@"5");

image.png

这里打印的结果为12354,接下来分析下

  • 因为这里是dispatch_barrier_sync同步栅栏函数,阻塞当前的线程,所以5一定是在3后面打印
  • 栅栏函数是在同一队列的任务,栅栏上方的任务先执行,当上方任务执行完毕再执行栅栏内部任务,最后执行栅栏下方任务
  • 所以1,2先打印,1,2的顺序不固定。接下来一定是打印3
  • 后面打印5,是因为dispatch_async本身存在耗时操作,4一定在5后面
异步栅栏函数
    
    dispatch_queue_t concurrentQueue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
    /* 1.异步函数 */
    dispatch_async(concurrentQueue, ^{
        NSLog(@"1");
    });
    
    dispatch_async(concurrentQueue, ^{
        
        NSLog(@"2");
    });
    /* 2. 栅栏函数 */ // 
    dispatch_barrier_async(concurrentQueue, ^{
        NSLog(@"3");
    });
    /* 3. 异步函数 */
    dispatch_async(concurrentQueue, ^{
        NSLog(@"4");
    });
    // 4
    NSLog(@"5");

image.png

这里的打印顺序为51234,接下来分析下

  • 观察打印顺序和同步栅栏函数唯一的区别是5的打印
  • 是因为异步栅栏函数不会阻塞当前线程,而dispatch_async存在耗时,所以5先打印,剩下的顺序与同步栅栏函数一致
栅栏函数底层分析

通过libdispatch源码进入函数dispatch_barrier_sync->_dispatch_barrier_sync_f->_dispatch_barrier_sync_f_inline
进入_dispatch_barrier_sync_f_inline

image.png

  • _dispatch_sync_f_slow
  • _dispatch_sync_recurse
  • _dispatch_lane_barrier_sync_invoke_and_complete
    这里有这3个方法我们不知道最终进入哪个,我们通过符号断点来确认
    image.png

    进入_dispatch_sync_f_slow
    image.png

    继续跟踪流程,并添加_dispatch_sync_invoke_and_complete_recurse的符号断点,并进入
    image.png

    进入_dispatch_sync_complete_recurse
    image.png

    这里我们发现是个do while循环,这里思考下为什么这么写?
  • 栅栏函数起到的的是同步作用,同一队列中,栅栏前的任务没有执行栅栏函数是不会走的
  • 所以这里需要递归处理栅栏函数前面的任务
  • _dispatch_sync_complete_recurse中的递归中,先判断barrier是否存在,如果存在则需要先把栅栏前的任务dx_wakeup 全部唤醒。唤醒成功后才会执行_dispatch_lane_non_barrier_complete

先来查看dx_wakeup,来查看barrier什么时候被移出,dx_wakeup是通过宏定义的函数

#define dx_wakeup(x, y, z) dx_vtable(x)->dq_wakeup(x, y, z)

搜索dq_wakeup

image.png

由上图可知串行队列和并行队列都走了_dispatch_lane_wakeup,而全局并发队列走了_dispatch_root_queue_wakeup

  • 串行、并行队列,进入_dispatch_lane_wakeup
    image.png

    进入_dispatch_lane_barrier_complete
    image.png
  • 如果是串行队列则会进行等待,知道其他任务执行完成,按顺序执行
  • 如果是并行队列,调用_dispatch_lane_drain_non_barriers将栅栏前的任务按照异步的放心执行
  • 全局并发队列
    当是全局并发队列的时候,进入_dispatch_root_queue_wakeup
    image.png

    由上图可知,全局并发队列并没有栅栏函数的相关处理流程,这也是栅栏函数在全局并发队列失效的原因

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

信号量(dispatch_semaphore_t)

  • dispatch_semaphore_create创建一个Semaphore,并初始信号总量
  • dispatch_semaphore_wait信号量减1,当信号量小于0时,就会所在线程发生阻塞,大于等于0时,正常执行
  • dispatch_semaphore_signal信号量加1
案例

image.png

这里我i们的正常理解应该是先执行任务1,但是这里初始化的信号总量为0,且在任务1中dispatch_semaphore_wait起到了加锁作用,所以先去执行任务2,且发出了dispatch_semaphore_signal解锁信号,再去执行任务`

信号量底层分析

dispatch_semaphore_wait

image.png

首先对信号量做减1操作,当信号量大于等于0时直接返回,否则进入_dispatch_semaphore_wait_slow方法
image.png

timeout进行判断

  • 如果是默认的,会直接跳出
  • DISPATCH_TIME_NOW,会进行一个超时处理
  • DISPATCH_TIME_FOREVER会进入_dispatch_sema4_wait方法
    进入_dispatch_sema4_wait方法
void
_dispatch_sema4_wait(_dispatch_sema4_t *sema)
{
    int ret = 0;
    do {
        ret = sem_wait(sema);
    } while (ret == -1 && errno == EINTR);
    DISPATCH_SEMAPHORE_VERIFY_RET(ret);
}

我们发现有个do while方法,并调用sem_wait,全局搜索sem_wait

image.png

并没有搜索出sem_wait方法的实现
所以_dispatch_sema4_waitdo while就是个死循环,原因就是要让该任务一致处于等待状态

dispatch_semaphore_signal

image.png

通过os_atomic_inc2o对信号量做+1操作,如果大于0直接返回。
如果加过一次后仍小于0,则会抛出异常Unbalanced call to dispatch_semaphore_signal()并调用_dispatch_semaphore_signal_slow方法,见下图:
image.png

这里会开启一个循环,对信号量加一操作,知道满足条件位置

【总结】信号来那个在实际开发中的作用

  • 保持线程同步,将异步执行的任务,转换成同步操作
  • 保证线程安全,为线程加锁(自旋锁)

调度组

dispatch_group_create创建组
dispatch_group_async进组任务
dispatch_group_notify进组任务执行完毕通知
dispatch_group_wait 进组任务执行等待时间

dispatch_group_enter 进组
dispatch_group_leave 出组
两者搭配使用

案例实现1

image.png

我们在异步线程中加了个sleep(2),这个时候在主线程打印为空数组,但是在我们的dispatch_group_notify中是能够打印出数组的内容。是因为在相同组中的任务,都执行完毕后会走dispatch_group_notify该方法。
【注意】dispatch_group_enterdispatch_group_leave要成对出现,并且dispatch_group_enter在执行任务前,dispatch_group_leave任务执行完成后调用,否则顺序错误会报错

案例实现2

image.png

这里直接将任务放在dispatch_group_async,最终结果和上述案例相同,其实dispatch_group_async就是底层封装了dispatch_group_enter 和dispatch_group_leave

调度组底层分析

dispatch_group_create

image.png

调用_dispatch_group_create_with_count并将信号量默认传0
image.png

通过os_atomic_store2o进行保存

dispatch_group_enter

image.png

默认信号量为0 ,所以信号量减1,由0-1,old_bits等于-1

dispatch_group_leave

image.png

信号量加1,此时的newState等于0oldState等于-1

#define DISPATCH_GROUP_VALUE_MASK       0x00000000fffffffcULL
#define DISPATCH_GROUP_VALUE_1          DISPATCH_GROUP_VALUE_MASK

old_state & DISPATCH_GROUP_VALUE_MASK等于0,即old_value等于0
也就是 old_value 与DISPATCH_GROUP_VALUE_1不会相等,最终调用if中的 return _dispatch_group_wake_dispatch_group_wake也就是去唤醒dispatch_group_notify

dispatch_group_notify

image.png

这里判断old_state == 0就去唤醒函数的执行流程,在上一步已经分析出old_state = 0

所以这里也就解释了dispatch_group_enter和dispatch_group_leave为什么要配合起来使用

你可能感兴趣的:(OC底层原理探索—GCD(下)—— 栅栏函数、调度组、信号量)