GCD剖析

GCD剖析图.png

一、首先我们再大致介绍一下GCD:

1、GCD 是什么?

GCD全称Grand Central Dispatch,GCD 在后端管理着一个线程池。GCD 不仅决定着你的代码块将在哪个线程被执行,它还根据可用的系统资源对这些线程进行管理。这样可以将开发者从线程管理的工作中解放出来,通过集中的管理线程,来缓解大量线程被创建的问题。

2、 GCD的改变?

GCD 带来的另一个重要改变是,作为开发者可以将工作考虑为一个队列,而不是一堆线程,这种并行的抽象模型更容易掌握和使用。当多个队列要处理块时,系统可以自由分配额外的线程来同时调用块。当队列变空时,这些线程会自动释放.
从 iOS7 升到 iOS8 后,GCD 出现了一个重大的变化:在 iOS7 时,使用 GCD 的并行队列, dispatch_async 最大开启的线程一直能控制在6、7条,线程数都是个位数,然而 iOS8后,最大线程数一度可以达到40条、50条。

3、GCD相比其他多线程有哪些优点?

GCD 能通过推迟昂贵计算任务并在后台运行它们来改善你的应用的响应性能。
GCD 提供一个易于使用的并发模型而不仅仅只是锁和线程,以帮助我们避开并发陷阱。
GCD 具有在常见模式(例如单例)上用更高性能的原语优化你的代码的潜在能力。
GCD 会自动利用更多的CPU内核(比如双核、四核)

4、GCD术语

串行(Serial):让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
并发(Concurrent):可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)并发功能只有在异步(dispatch_async)函数下才有效。
同步(Synchronous):在当前线程中执行任务,不具备开启新线程的能力
异步(Asynchronous):在新的线程中执行任务,具备开启新线程的能力

    1、首先,系统提供给你一个叫做 主队列(main queue) 的特殊队列。和其它串行队列一样,这个队列中
       的任务一次只能执行一个。然而,它能保证所有的任务都在主线程执行,而主线程是唯一可用于更新 UI 的线程。
       这个队列就是用于发生消息给 UIView 或发送通知的。
     
     系统同时提供给你好几个并发队列。它们叫做 全局调度队列(Global Dispatch Queues) 。
     目前的四个全局队列有着不同的优先级:background、low、default 以及 high。要知道,Apple 的 API 也会使用这些队列,
     所以你添加的任何任务都不会是这些队列中唯一的任务。
     
     最后,你也可以创建自己的串行队列或并发队列。
        主队列、
        串行队列、
        并发队列。
    /*
     1 :GCD队列类型:
     
     类型 描述
     Serial 串行队列将任务以先进先出(FIFO)的顺序来执行,所以串行队列经常用来做访问某些特定资源
      的同步处理。你可以也根据需要创建多个队列,而这些队列相对其他队列都是并发执行的。换句话说,
如果你创建了4个串行队列,每一个队列在同一时间都只执行一个任务,对这四个任务来说,他们是相互独立且并发执行的。
如果需要创建串行队列,一般用dispatch_queue_create这个方法来实现。
     Concurrent 并发队列虽然是能同时执行多个任务,但这些任务仍然是按照先到先执行(FIFO)的顺序来执行的。
并发队列会基于系统负载来合适地选择并发执行这些任务。在iOS5之前,并发队列一般指的就是全局队列(Global queue),
进程中存在四个全局队列:高、中(默认)、低、后台四个优先级队列,可以调用dispatch_get_global_queue函数传入优先级来访问队列。
而在iOS5之后,我们也可以用dispatch_queue_create,并指定队列类型DISPATCH_QUEUE_CONCURRENT,来自己创建一个并发队列。
   
     */
    // 异步全局队列: Concurrent 又称为global dispatch queue,可以并发地执行多个任务,但是执行完成的顺序是随机的。
    // 同步主队列: Main dispatch queue   与主线程功能相同。实际上,提交至main queue的任务会在主线程中执行。main queue可以调用
dispatch_get_main_queue()来获得。因为main queue是与主线程相关的,所以这是一个串行队列。和其它串行队列一样,
这个队列中的任务一次只能执行一个。它能保证所有的任务都在主线程执行,而主线程是唯一可用于更新 UI 的线程。
    
    //[self dispatch_serialQueue];
    
    //[self dispatch_concurrentQueue];
    
    
    //2: GCD里面有多少种全局队列?
    /*
      Global Dispatch Queue有如下8种:
     
         Global Dispatch Queue (High priority)
         Global Dispatch Queue (Default priority)
         Global Dispatch Queue (Low priority)
         Global Dispatch Queue (Background priority)
         Global Dispatch Queue (High overcommit priority)
         Global Dispatch Queue (Default overcommit priority)
         Global Dispatch Queue (Low overcommit priority)
         Global Dispatch Queue (Background overcommit priority)
     
       注意前面四种 和后面四种不同优先级的Queue有一词之差:Overcommit。其区别就在于Overcommit Queue不管系统状态如何都会强制生成线程队列。

二、死锁情况:

死锁情况1
  • 原因:
    dispatch_sync表示同步的执行任务,也就是说执行dispatch_sync后,当前队列会阻塞。而dispatch_sync中的block如果要在当前队列中执行,就得等待当前队列程执行完成。

    在下面这个例子中,主队列在执行dispatch_sync,随后队列中新增一个任务block。因为主队列是同步队列,所以block要等dispatch_sync执行完才能执行,但是dispatch_sync是同步派发,要等block执行完才算是结束。在主队列中的两个任务互相等待,导致了死锁。

- (void)dispatchQueueQuestion{
    NSLog(@"current thread = %@", [NSThread currentThread]);

    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    dispatch_sync(mainQueue,^{
        NSLog(@"current thread = %@", [NSThread currentThread]);
        NSLog(@"循环等待主队列导致死锁,该句话不会被执行");
    });
}
死锁情况2
  • 分析:
    1、执行任务1。
    2、异步线程,不阻塞主线程,所以任务5和任务2会同时执行,所以输出顺序不定。
    3、任务2执行完毕之后,遇到同步线程,此时任务3在同步线程里面。
    4、但是任务2和任务4是在一个线程里,且任务4本先于任务3加入串行队列,应该执行完任务2就执行任务4,但是被任务3阻塞了,而任务3又在任务4后面执行。这样就造成了循环等待的问题,线程死锁。
dispatch_queue_t queue = dispatch_queue_create("com.demo.serialQueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"1"); // 任务1
dispatch_async(queue, ^{
    NSLog(@"2"); // 任务2
    dispatch_sync(queue, ^{  
        NSLog(@"3"); // 任务3
    });
    NSLog(@"4"); // 任务4
});
NSLog(@"5"); // 任务5
未死锁的情况:

任务执行分析:
1、任务1,异步线程,任务5加入到主队列中。
2、先执行任务1,由于异步线程,所以不会阻塞主线程,任务2,任务5会同时执行,5,2输出顺序不定。
3、任务3在同步主线程里,此时的任务3在任务5之后,阻塞当前线程,会先执行任务3。由于阻塞完成,所以继续执行任务4。

- (void)notZuSe{    
    NSLog(@"1"); // 任务1
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"2"); // 任务2
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"3"); // 任务3
        });
        NSLog(@"4"); // 任务4
    });
    NSLog(@"5"); // 任务5
}


三、循环执行dispatch_apply

  • dispatch_apply 会重复的执行,类似于for循环,内部执行会开启新线程,有些也会在主线程中执行。
- (void)dispatchApply{
   
    dispatch_apply(10, dispatch_get_global_queue(0, 0), ^(size_t index) {
        NSLog(@"---------重复执行线程为:%@",[NSThread currentThread]);
    });

}

四、创建串行队列

- (void)dispatch_serialQueue{
    //串行队列
    dispatch_queue_t serialQueue;
    serialQueue = dispatch_queue_create("com.example.SerialQueue", DISPATCH_QUEUE_SERIAL);// dispatch_queue_attr_t设置成NULL的时候默认代表串行。
    dispatch_async(serialQueue, ^{
         // something
         NSLog(@"current thread = com.example.SerialQueue  %@", [NSThread currentThread]);
    });
}

五、创建并发队列

- (void)dispatch_concurrentQueue{
    //并发队列
    dispatch_queue_t concurrentQueue;
    concurrentQueue = dispatch_queue_create("com.example.ConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // something
        NSLog(@"current thread = com.example.ConcurrentQueue %@", [NSThread currentThread]);
    });
}

六、获取主队列

- (void)dispath_mainQueue{
    // 主队列:
    dispatch_queue_t mainQueue;
    mainQueue = dispatch_get_main_queue();
    
    dispatch_async(dispatch_get_main_queue(), ^{
        // something
        NSLog(@"current thread = %@  main ", [NSThread currentThread]);
        
    });
}

七、创建自定义queue

- (void)dispatch_queue_t{
    // 自定义dispatch_queue_t
    dispatch_queue_t emailQueue = dispatch_queue_create("www.summerHearts@163.com", NULL);
    dispatch_async(emailQueue, ^{
        NSLog(@"current thread = %@", [NSThread currentThread]);
    });
}

八、信号量dispatch_semaphore_t

信号量是什么能做什么?

1、信号量是一种老式的线程概念。
2、信号量能让你控制多个消费者对有限资源的访问。举例:如果你创建了一个有两个资源的信号量,那么同时最多只能有两个线程可以访问临界区。其他想使用资源的线程必须在一个FIFO队列里等待。

dispatch Semaphore 理解:

dispatch Semaphore 是持有计数的信号,该计数是多线程编程中的计数类型信号。所谓信号,就是过马路时常用的手旗。可以通过时举起手旗,不可通过时放下手旗。使用计数来实现该功能。计数为0时等待,计数为1或者大于1,减去1而不等待。

- (void)dispatch_semaphore{
    dispatch_queue_t queues = dispatch_queue_create(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    
    NSMutableArray *array = [NSMutableArray array];
    
    for (int i = 0; i <  10; ++i) {
        dispatch_async(queues, ^{
            /*
             * 等待 Dispatch Semaphore.
             * 一直等待,直到Dispatch Semaphore的计数达到大于等于1
             *  // 由于是异步执行的,所以每次循环Block里面的dispatch_semaphore_signal根本还没有执行就会执行dispatch_semaphore_wait,从而semaphore-1.当循环10此后,semaphore等于0,则会阻塞线程,直到执行了Block的dispatch_semaphore_signal 才会继续执行
             */
            
            long result =  dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            NSLog(@">>>>   %ld",result);
            
            /*
                此时 result为0, 可安全的执行需要进行排他控制的处理。该处理结束的时候通过dispatch_semaphore_signal函数将dispatch_semaphore的计数值加1.
             */
            /*
             由于Dispatch semaphore 的计数值达到大于或者等于等一1,所以将 Dispatch semaphore的计数值减去1. dispatch_semaphore_wait函数执行返回。即执行到此,Dispatch semaphore 的计数值为0.
             
             */
            [array addObject:[NSNumber numberWithInt:i]];
            
            /*
             dispatch_semaphore_signal函数将dispatch semaphore 的计数值加1.如果有通过 dispatch_semaphore_wait函数等待 dispatch semaphore的计数值增加的线程就由最先等待的线程执行。
             */
            dispatch_semaphore_signal(semaphore);
        });
            //dispatch_semaphore_wait 函数等待Dispatch Semaphore的计数值达到大于或者等于1.当计数值大于等于1,或者在待机中技术值大于或者等于1,对该计数进行减法并从dispatch_semaphore_wait函数返回。
    }
}

大神就是大神,几乎是四年前的东西了,真的应该好好看动手写一下,自己的进步才会是飞速的。
关键字:
1、使用串行异步队列。
2、在开辟的异步线程dispatch_asyncA中嵌套异步线程dispatch_asyncB(注意Queue不同)。
3、在dispatch_asyncB之前设置等待,因为只有可用信号量的时候才能继续。

dispatch_semaphore_wait(limitSemaphore, DISPATCH_TIME_FOREVER);

4、在dispatch_asyncB内部执行完毕之后,释放信号量。

dispatch_semaphore_signal(limitSemaphore);

下面是信号量主要方法的实现,非阻塞线程的奥:


- (void)viewDidLoad {
    [super viewDidLoad];
    //因为用到了dispatch_barrier_async,该函数只能搭配自定义并行队列dispatch_queue_t使用。所以不能使用:dispatch_get_global_queue
    dispatch_queue_t queue = dispatch_queue_create("com.ioschengxuyuan.gcd.ForBarrier", DISPATCH_QUEUE_CONCURRENT);
    /*
     *
     *生成Dispatch Semaphore
     Dispatch Semaphore 的计数初始值设定为“1”
     (该初始值的1与下文中两个函数dispatch_semaphore_wait与dispatch_semaphore_signal进行的减1、加1里的1没有必然联系。
     
     就算初始值是100,两个函数dispatch_semaphore_wait与dispatch_semaphore_signal还是会减“1”、加“1”)。
     保证可访问 NSMutableArray 类对象的线程
     同时只能有1个
     *
     */
    NSMutableArray *array = [[NSMutableArray alloc] init];
    for(int i = 0; i< 100000; ++i) {
        dispatch_async_limit(queue, 1, ^{
            /*
             *
             *等待Dispatch Semaphore
             *一直等待,直到Dispatch Semaphore的计数值达到大于等于1
             */
            //            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER) ;
            /*
             *由于Dispatch Semaphore的计数值达到大于等于1
             *所以将Dispatch Semaphore的计数值减去1
             *dispatch_semaphore_wait 函数执行返回。
             *即执行到此时的
             *Dispatch Semaphore 的计数值恒为0
             *
             *由于可访问NSMutaleArray类对象的线程
             *只有一个
             *因此可安全地进行更新
             *
             */
            NSLog(@"🔴%@",[NSThread currentThread]);
            [array addObject:[NSNumber numberWithInt:i]];
            /*
             *
             *排他控制处理结束,
             *所以通过dispatch_semaphore_signal函数
             *将Dispatch Semaphore的计数值加1
             *如果有通过dispatch_semaphore_wait函数
             *等待Dispatch Semaphore的计数值增加的线程,
             ★就由最先等待的线程执行。
             */
            //            dispatch_semaphore_signal(semaphore);
        });
    }
    /*
     *
     等为数组遍历添加元素后,检查下数组的成员个数是否正确
     *
     */
    dispatch_barrier_async(queue, ^{
        NSLog(@"🔴类名与方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, @([array count]));
    });
}

调用的封装方法:

void dispatch_async_limit(dispatch_queue_t queue,NSUInteger limitSemaphoreCount, dispatch_block_t block) {
//控制并发数的信号量
    static dispatch_semaphore_t limitSemaphore;
    //专门控制并发等待的线程
    static dispatch_queue_t receiverQueue;
    
    //使用 dispatch_once而非 lazy 模式,防止可能的多线程抢占问题
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        limitSemaphore = dispatch_semaphore_create(limitSemaphoreCount);
        receiverQueue = dispatch_queue_create("receiver", DISPATCH_QUEUE_SERIAL);
    });
    
    dispatch_async(receiverQueue, ^{
        //可用信号量后才能继续,否则等待
        dispatch_semaphore_wait(limitSemaphore, DISPATCH_TIME_FOREVER);
        dispatch_async(queue, ^{
            !block ? : block();
            //在该工作线程执行完成后释放信号量
            dispatch_semaphore_signal(limitSemaphore);
        });
    });
}

信号量非阻塞主线程连接demo

九、栅栏dispatch_barrier_async

项目使用场景(19.4.23)

刚才还在考虑网络请求的延时会导致dispatch_barrier_async失效,一筹莫展怎么处理好呢?查来查去发现Dispatch_barrier_async根本就不是用来应对网络请求的场景的,栅栏主要是应对访问数据库或文件时,为了防止数据竞争,GCD才提供了一个更为有效的解决方法Dispatch_barrier_async函数,还是理解不到位啊。

栅栏的执行流程
  • 简介回答:通过dispatch_barrier_async函数提交的任务会等它前面的任务执行完才开始,然后它后面的任务必须等它执行完毕才能开始.
  • 详细执行流程:dispatch_barrier_async函数会等待追加到concurrent dispatch queue 上的并行执行的处理全部结束之后,再将制订的处理追加到该concurrent dispatch queue中。然后再由dispatch_barrier_async函数追加的处理执行完毕之后,concurrent dispatch queue才恢复过来一般的动作,追加到该concurrent dispatch queue的处理又开始并行执行。
栅栏的使用场景:
  • 必须使用dispatch_queue_create创建的队列才会达到上面的效果.dispatch_barrier_async 起到了“承上启下”的作用。它保证此前的任务都先于自己执行,此后的任务也迟于自己执行。正如barrier的含义一样,它起到了一个栅栏、或是分水岭的作用。
    这样一来,使用并行队列和 dispatc_barrier_async 方法,就可以高效的进行数据和文件读写了。
#pragma mark - 栅栏
- (void)dispatch_barrier_async{
 
    /*----------------------------------------------*/
    注意点:
    // 一、自定义串行队列,并发执行,此处只会开启一个线程,所有的操作都在本线程里面执行。
    // 二、自定义并发队列,并发执行,此处只会开启多个线程。在dispatch_barrier_async队列之前(1,2)以及之后(3,4)的任务是并发执行的,dispatch_barrier_async相当于分水岭,放在第几个执行就是第几个执行。
    dispatch_queue_t serialQueue = dispatch_queue_create("my.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(serialQueue, ^() {
        NSLog(@"dispatch_async--1:%@",[NSThread currentThread]);
    });
    
    dispatch_async(serialQueue, ^() {
        NSLog(@"dispatch_async--2:%@",[NSThread currentThread]);
    });
    
    dispatch_barrier_async(serialQueue, ^() {
        NSLog(@"dispatch-barrier_async:%@",[NSThread currentThread]);
//        sleep(1); 这样只会阻塞分线程,对主线程并没有什么影响。
    });
    
    // 虽然异步, 但是在串行队列中, 会按照顺序执行
    dispatch_async(serialQueue, ^() {
        NSLog(@"dispatch_async--3:%@",[NSThread currentThread]);
//        sleep(1);
    });
    
    dispatch_async(serialQueue, ^() {
        NSLog(@"dispatch_async--4:%@",[NSThread currentThread]);
    });
}

十、只执行一次dispatch_once_t

- (void)dispatch_once_t{
    // 一次性执行:
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    // code to be executed once,默认线程安全
    });
}

十一、延时执行dispatch_time_t

- (void)dispatch_queue_after{
    // 延迟2秒执行:
    double delayInSeconds = 2.0;
   dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        // code to be executed on the main queue after delay
    });
}

十二、线程组及dispatch_group_notify

如果要监控所有线程执行完毕,就需要调用dispatch_group_notify方法,在此方法中我们可以执行特定的操作。

- (void)dispatch_queue_group{
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, dispatch_get_global_queue(0,0), ^{
        // 并行执行的线程一
        NSLog(@"并行执行的线程一current thread = %@", [NSThread currentThread]);
    });
    dispatch_group_async(group, dispatch_get_global_queue(0,0), ^{
        // 并行执行的线程二
        NSLog(@"并行执行的线程2current thread = %@", [NSThread currentThread]);
    });
    dispatch_group_async(group, dispatch_get_global_queue(0,0), ^{
        // 并行执行的线程二
        NSLog(@"并行执行的线程3current thread = %@", [NSThread currentThread]);
    });
    //注意:当所有的线程执行完毕之后,才会执行notify线程。这里还是在子线程中。
    dispatch_group_notify(group, dispatch_get_global_queue(0,0), ^{
        
        NSLog(@"current thread = %@", [NSThread currentThread]);
        
    });
/*
如果开发者要在主线程中处理,可以换成主线程。
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@" dispatch_group_notify current thread = %@", [NSThread currentThread]);
    });
*/
}
  • 但是这种情况在网络请求时会有问题,因为网络请求都是异步的,dispatch_group_async只是在其它线程发送请求之后就算执行完毕了,并不会等待回调,所以我们并不知道它什么时候请求完成,所以无法满足我们的需求。
    此时,dispatch_group_enter(group)和dispatch_group_leave(group)就派上用场了,这种方式更为灵活,enter和leave必须配合使用,且一一对应,否则group会一直存在。当所有的enter和leave都执行完毕后,才会执行dispatch_group_notify。
dispatch_group_t group = dispatch_group_create();
    dispatch_group_enter(group);
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //请求1
        [网络请求:{
        成功:dispatch_group_leave(group);
        失败:dispatch_group_leave(group);
}];
    });
    dispatch_group_enter;
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //请求2
        [网络请求:{
        成功:dispatch_group_leave;
        失败:dispatch_group_leave;
}];
    });
    dispatch_group_enter(group);
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //请求3
        [网络请求:{
        成功:dispatch_group_leave(group);
        失败:dispatch_group_leave(group);
}];
    });
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        //界面刷新
        NSLog(@"任务均完成,刷新界面");
    });
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);

通过查找资料以及写代码对DCG相关的知识点做了总结,如果有不足的地方还希望读者们多多提醒。最后,让我们感谢一下那些知识的奉献者。
有关信号量的详细介绍:
https://www.jianshu.com/p/04ca5470f212
https://www.jianshu.com/p/888ea823c8a5
大神超详细分析的GCD:链接
GCD死锁实例
GCD死锁原因底层剖析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 谈到iOS多线程,一般都会谈到四种方式:pthread、NSThread、GCD和NSOperation。其中,苹...
    攻城狮GG阅读 285评论 0 3
  • 1.回顾自己朋友圈分享的10篇文章或链接,写下自己转发的具体动机是什么? 1.标题:《穿越千年的“姻缘”》(ID:...
    爱1绝版阅读 326评论 0 0
  • 曾经自己坚定地声称自己是义务论者,甚至因为这种幼稚的想法和朋友吵过一架,一个晚上没说话。可是直到今天才发现,原来自...
    wwwbb阅读 580评论 2 0