GCD-死锁-单例-栅栏-信号量-调度组

1,028 阅读7分钟

死锁

  • 写一个串行队列的异步线程任务,再加一个同步线程任务,发生死锁报错
- (void)textDemo1{
    
    dispatch_queue_t queue = dispatch_queue_create("cooci", NULL);
    NSLog(@"1");
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
}
结果是 152正常执行完成后发生死锁
分析
1.串行队列任务是先进先出
2.dispatch_async 块任务中加入了 2 dispatch_sync 块任务 和 4 ,其中 dispatch_sync中要执行3,队列中任务顺序是 152、 同步块、 4 33.2完成后执行dispatch_sync任务,同步函数需要执行3才能执行4,但是43任务之前加入的,依据先进先出的原则,只有执行完4才能执行3,任务之间互相等待产生死锁问题。
  • 报错堆栈信息 image.png

  • 在函数dispatch_sync_f_slow函数之后 image.png

  • 找到报错函数 image.png

image.png

在队列上调用dispatch_sync "已被当前线程拥有"

  • 死锁的条件 dq_state,dsc->dsc_waiter 相同时,需要调用的线程是一个等待的线程,产生死锁。
if (unlikely(_dq_state_drain_locked_by(dq_state, dsc->dsc_waiter))) {

DISPATCH_CLIENT_CRASH((uintptr_t)dq_state,

"dispatch_sync called on queue "

"already owned by current thread");

}

单例原理分析

  • val参数为全局静态变量,block参数为任务封装,l是这个全局静态变量的门属性 image.png

  • dispatch_once_f image.png

  • 线程锁,线程安全 image.png

  • return _dispatch_once_callout(l, ctxt, func); 执行block中的任务

  • 完成任务之后进行关门处理 image.png

  • 对两个宏的赋值处理 image.png

  • 标记位done image.png

  • return _dispatch_once_wait(l);如果当前状态还没有完成,同时也没有标记位done,则进入等待

栅栏函数

最直接的作用:控制任务执行顺序,同步

  • dispatch_barrier_async 前面的任务执行完毕才会到这里

  • dispatch_barrier_sync 作用相同,但是这个会阻塞线程,影响后面的任务执行

  • 栅栏函数只能控制同一并发队列,全部并发队列不允许。

  • 底层分析 image.png

  • _dispatch_barrier_sync_f image.png

  • _dispatch_barrier_sync_f_inline image.png 进入_dispatch_sync_f_slow或_dispatch_sync_recurse

  • _dispatch_sync_recurse image.png 这里有一个do-while的死循环递归只有当前队列的任务全部清空完成后才能走下一步 _dispatch_sync_invoke_and_complete_recurse

  • _dispatch_sync_invoke_and_complete_recurse image.png

  • _dispatch_sync_complete_recurse image.png do-while 判断是否存在barrier,存在则dx_wakeup把前面的任务都唤醒执行,完成之后进入_dispatch_lane_non_barrier_complete表示当前已经完成了并且没有了barrier

  • _dispatch_lane_non_barrier_complete image.png 进行状态修复,否则一直dx_wakeup死循环

  • dx_wakeup 就是dq_wakeup, image.png

  • 会根据不同的队列类型赋值不同的函数,进行不同的函数调用, image.png

    • 全局并发队列则调用_dispatch_root_queue_wakeup
    • 普通并发队列则调用_dispatch_lane_wakeup
    • 查看两个函数的不同类解释为什么全局并发队列不能执行栅栏函数。
  • _dispatch_root_queue_wakeup image.png

  • _dispatch_lane_wakeup image.png

    • 判断当前队列中是否有barrier是则进入_dispatch_lane_barrier_complete栅栏完成函数,

    image.png

    • 如果是同步队列 dq_width = 1 则开始等待
    • 如果是异步队列进入_dispatch_lane_drain_non_barriers image.png 做一个do-while循环保证上面的任务完成后进入_dispatch_lane_non_barrier_complete_finish移除队列中的栅栏
  • 最后进入_dispatch_lane_class_barrier_complete 完成栅栏函数中的任务。 image.png 清空队列中的barrier标记,按正常流程完成任务。 由于全局并发队列中还有系统任务后台任务的函数需要处理,所以加栅栏堵塞,后台任务就无法执行,影响了这个系统的处理。

  • 栅栏的缺点,由于业务网络请求一般都是使用的AFN网络请求,队列的创建在框架中完成,我们无法获取网络请求的队列,所以栅栏函数使用会比较麻烦。

信号量dispatch_semaphore_t

  • dispatch_semaphore_create 创建信号量
  • dispatch_semaphore_wait 信号等待
  • dispatch_semaphore_signal 信号量释放
  • 同步->当锁,控制GCD最大并发数,可以控制一次完成的任务数量
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_semaphore_t sem = dispatch_semaphore_create(1);
    dispatch_queue_t queue1 = dispatch_queue_create("cooci", NULL);

    //任务1
 

    dispatch_async(queue, ^{
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); // 等待

        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); // 发信号
    });
    
    //任务3
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
        sleep(2);

        NSLog(@"执行任务3");
        NSLog(@"任务3完成");
        dispatch_semaphore_signal(sem);
    });


    //任务4
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
        sleep(2);

        NSLog(@"执行任务4");
        NSLog(@"任务4完成");
        dispatch_semaphore_signal(sem);
    });

分析

  • 信号量创建只要大于或等于0都是有用的否则没用 image.png

  • dispatch_semaphore_wait image.png os_atomic_dec2o = --1 value>=0则重置为0, 当我创建的信号量=0时,则进入_dispatch_semaphore_wait_slow函数

  • _dispatch_semaphore_wait_slow image.png 进入一个switch判断时长,如果是错误的不符合规则,则跳出去, DISPATCH_TIME_NOW,则超时处理,DISPATCH_TIME_FOREVER``则进入_dispatch_sema4_wait

  • _dispatch_sema4_wait image.png 是一个do-while循环的等待

  • dispatch_semaphore_signal image.png os_atomic_inc2o = ++1,value>0则重置为0,正常执行,如果加1后还是小于0,报出异常,dispatch_semaphore_wait操作过多,则进入_dispatch_semaphore_signal_slow

  • _dispatch_semaphore_signal_slow image.png 信号量创建+1,

结论:信号量的原理就是value值++和--,当值小于0进入do-while循环等待这个任务就进入等待,等待信号量变为正,当另外一个任务完成后,++之后信号量大于0,则进入下一个任务。

调度组

作用:控制任务执行顺序 dispatch_group_create 创建组 dispatch_group_async 进组任务 dispatch_group_notify 进组任务执行完毕通知 dispatch_group_wait 进组任务执行等待事件 dispatch_group_enter 进组 dispatch_group_leave 出组

  • 调度组的简单使用
- (void)viewDidLoad {
    [super viewDidLoad];

    self.imageView = [[UIImageView alloc] initWithFrame:CGRectMake(20, 300, 300, 200)];
    self.imageView.image = [UIImage imageNamed:@"backImage"];
    [self.view addSubview:self.imageView];
    
    [self groupDemo];
}


/**
 调度组测试
 */
- (void)groupDemo{
    
//    dispatch_group_enter(group);
//    dispatch_group_leave(group);
    
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_group_async(group, queue, ^{
        //创建调度组
        NSString *logoStr1 = @"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/09f14cef6f3d4859a85610da67ed38de~tplv-k3u1fbpfcp-watermark.image?";
        NSData *data1 = [NSData dataWithContentsOfURL:[NSURL URLWithString:logoStr1]];
        UIImage *image1 = [UIImage imageWithData:data1];
        [self.mArray addObject:image1];
    });


//    dispatch_group_async(group, queue, ^{
//        //创建调度组
//       NSString *logoStr2 = @"https://f12.baidu.com/it/u=3172787957,1000491180&fm=72";
//        NSData *data2 = [NSData dataWithContentsOfURL:[NSURL URLWithString:logoStr2]];
//        UIImage *image2 = [UIImage imageWithData:data2];
//        [self.mArray addObject:image2];
//    });
    
//      进组和出租 成对  先进后出
//    dispatch_group_leave(group);
    dispatch_group_enter(group);

    dispatch_async(queue, ^{
        //创建调度组
       NSString *logoStr2 = @"https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/09f14cef6f3d4859a85610da67ed38de~tplv-k3u1fbpfcp-watermark.image?";
        NSData *data2 = [NSData dataWithContentsOfURL:[NSURL URLWithString:logoStr2]];
        UIImage *image2 = [UIImage imageWithData:data2];
        [self.mArray addObject:image2];
//        dispatch_group_enter(group);
        dispatch_group_leave(group);

    });
    
//    long time = dispatch_group_wait(group, 1);
//
//    if (time == 0) {
//
//    }
    
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        UIImage *newImage = nil;
       NSLog(@"数组个数:%ld",self.mArray.count);
       for (int i = 0; i<self.mArray.count; i++) {
           UIImage *waterImage = self.mArray[i];
           newImage =[KC_ImageTool kc_WaterImageWithWaterImage:waterImage backImage:newImage waterImageRect:CGRectMake(20, 100*(i+1), 100, 40)];
       }
        self.imageView.image = newImage;
    });

}
  • 实现图片的水印效果 image.png
  • 问题1:调度组是如何控制流程的
  • 问题2:调度组进组和出组的搭配奔溃问题
  • 问题3:dispatch_group_async = dispatch_group_enter+dispatch_group_leave

底层分析

  • dispatch_group_create image.png

image.png

  • dispatch_group_enter(dispatch_group_t dg) image.png 这里传入的dg传入的是0这里的一个--操作信号量变为-1

  • dispatch_group_leave image.png 这里的操作是将-1变为0,os_atomic_add_orig2o这个是+1的操作, image.png

  • 1.加入dg=-1,old_state = -1 + 1 = 0,old_value与操作后就是0

  • 2.加入dg=0, old_state = 1 + 1 = 1,old_value与操作后就是1 image.png

  • 操作后=0时,进入判断后显然0不等于DISPATCH_GROUP_VALUE_1,do-while循环处理状态操作后,进入_dispatch_group_wake唤醒dispatch_group_notify操作,否则1的时候就是进入下面的判断,报错处理

  • _dispatch_group_notify image.png 判断old_state==0才会_dispatch_group_wake唤醒 整个流程可以简略为0 - 1后阻塞,+1后唤醒下一步操作

  • dispatch_group_async image.png

  • _dispatch_continuation_group_async image.png 这里有一个进组的操作将原始的信号量0变成-1

  • 流程分析 image.png

image.png

  • 假设现在是全局并发队列一下流程是执行block的流程分析 image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

  • 如果这里是有group的标签 image.png

  • _dispatch_continuation_with_group_invoke image.png 这里执行了离开组的操作,

  • 执行block image.png 总结:_dispatch_continuation_group_async其实内部自动执行了进组与出组的操作

在日常的开过程中,我们经常会用到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 挂起 两个重要的参数:

  • 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 监视事件,写入字节的缓冲区空间