GCD的使用和原理

作者: 翰霖啊 | 来源:发表于2018-09-05 17:01 被阅读115次

    在我们做iOS开发的过程中,经常会与多线程打交道,异步绘制,网络请求等,方式有NSThread,NSOperationQueue,GCD等,在这里GCD的地位举足轻重,那么今天写一篇关于GCD的文章。首先迎来第一个问题:

    什么是GCD
    全名叫 Grand Central Dispatch 是一种异步执行任务的技术,一套基于c语言实现的api,语法十分简洁,只需要简单定义任务或按需加入到队列中,就可以按照计划实现功能,下面一个简单的例子。

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
            //网络请求
            //异步绘制图像
            //数据库访问等
            dispatch_async(dispatch_get_main_queue(), ^{
                //刷新UI
            });
        });
    

    在这里使用异步的方式定义了一个任务,并将其添加到一个全局队列中,这里面的任务内容可以进行一些耗时操作,由于是异步所以不影响主线程,当任务结束之后,同样使用异步的方式定义了一个任务,将其添加到了主队列中,这里执行的任务会在主线程完成。

    同步和异步
    前面内容提到了“异步”一词,那么这里聊一下什么是“异步”,相应的还有“同步”一词。
    异步的作用是不需要CPU立刻响应,而是等待一个信号或回调来下一步操作,当前线程可以立刻执行后续内容,而同步为可以阻塞当前线程,当执行完一个任务之后才能执行下一个。
    异步是目的,多线程是手段
    当我们开启一个新的线程之后,可以将任务交由新的线程执行,实现异步效果,当需要的时候再返回之前的线程。

    用图来做一个同步和异步的解释


    异步和同步

    这里thread1模拟主线程,当执行任务A的时候由于耗时比较短,所以可以在主线程上完成,当执行到任务B的时候,由于耗时长,所以开辟了一条新的线程,将任务交由子线程处理,同时继续完成任务C,当耗时操作完成之后,将结果返回给主线程进行操作,实现了异步功能。

    使用多线程仍然要注意几个问题,1.数据竞争 2.死锁 3.线程开辟太多导致内存消耗过大,不过只要使用得当,多线程对我们开发的好处十分巨大。

    串行队列和并发队列
    首先解释一下“队列”,如其名字,它是执行处理的等待队列,这里的队列调度方式为先进先出模式(FIFO-First In First Out),也就是先添加到队列中的任务先执行,当然还有其他几种模式,不过这里暂时不考虑,有兴趣的请自行查资料

    FIFO
    队列的种类也是两种,Serial Dispatch Queue和 Concurrent Dispatch Queue,分别为串行队列和并发队列,串行队列中的任务必须按照顺序依次执行,并发队列可以多个线程以并发的形式执行。
    串行队列和并发队列
    下面用代码来看串行队列和并发队列的区别,首先使用串行队列的方式创建几个异步操作
    dispatch_queue_t queue = dispatch_queue_create("com.example.gcdDemo", DISPATCH_QUEUE_SERIAL);
        dispatch_async(queue, ^{
            NSLog(@"%@---0", [NSThread currentThread]);
        });
        dispatch_async(queue, ^{
            NSLog(@"%@---1", [NSThread currentThread]);
        });
        dispatch_async(queue, ^{
            NSLog(@"%@---2", [NSThread currentThread]);
        });
        dispatch_async(queue, ^{
            NSLog(@"%@---3", [NSThread currentThread]);
        });
    

    看看输出结果

    <NSThread: 0x6000002777c0>{number = 3, name = (null)}---0
    <NSThread: 0x6000002777c0>{number = 3, name = (null)}---1
    <NSThread: 0x6000002777c0>{number = 3, name = (null)}---2
    <NSThread: 0x6000002777c0>{number = 3, name = (null)}---3
    

    很明显,这里是同一个线程,而且也确实是按照顺序执行的,那么接下来使用并发队列来操作。

    dispatch_queue_t queue = dispatch_queue_create("com.example.gcdDemo", DISPATCH_QUEUE_CONCURRENT);
        dispatch_async(queue, ^{
            NSLog(@"%@---0", [NSThread currentThread]);
        });
        dispatch_async(queue, ^{
            NSLog(@"%@---1", [NSThread currentThread]);
        });
        dispatch_async(queue, ^{
            NSLog(@"%@---2", [NSThread currentThread]);
        });
        dispatch_async(queue, ^{
            NSLog(@"%@---3", [NSThread currentThread]);
        });
    

    再看看输出结果

    <NSThread: 0x60400046adc0>{number = 5, name = (null)}---2
    <NSThread: 0x60400046b000>{number = 3, name = (null)}---0
    <NSThread: 0x6000004655c0>{number = 4, name = (null)}---1
    <NSThread: 0x6000004653c0>{number = 6, name = (null)}---3
    

    很明显,并没有按照顺序执行,而且不是同一个线程,这就印证了上面所说的串行队列和并发队列的区别。
    但是不代表创建了多少个并发任务就会开辟多少个线程,这个线程数量由XNU内核来决定,将上面的案例调整为8个任务,可以看到有些线程是重复的,表明了如果一开始就可以开辟8个线程,就不会出现这种情况,所以当一个任务结束之后,下一个任务可以放到完成的线程中,但是和串行队列还是有区别的


    多个任务的并发队列

    首先会开辟4条线程处理任务,将task0-task3异步形式放入线程中执行,假定task0首先完成,并发队列中取出task4放入线程3中执行,以此类推,所以也就造成了有些线程是重复的原因。

    DISPATCH MAIN QUEUE/DISPATCH GLOBAL QUEUE
    我们平时常用的大多是这两种,前者是在主线程执行的queue,由于主线程只有一个,自然是个串行队列,而global_queue是个并发队列,这个队列有4个优先级,分别是低优先级(DISPATCH_QUEUE_PRIORITY_LOW),默认优先级(DISPATCH_QUEUE_PRIORITY_DEFAULT),高优先级(DISPATCH_QUEUE_PRIORITY_HIGH),和后台优先级(
    DISPATCH_QUEUE_PRIORITY_BACKGROUND),代码如下

    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
    

    GCD的api

    前面已经提到一些关于GCD的api,下面会详细的探讨这些api的功能
    dispatch_queue_create

    dispatch_queue_t serialDispatchQueue = dispatch_queue_create("com.example.gcdDeo",NULL);
    

    这里是创建一个queue的方法,返回类型为dispatch_queue_t,方法含有两个参数,一个是队列名,推荐使用应用程序ID逆序的域名方式,后面参数表示生成的队列类型,一种为DISPATCH_QUEUE_SERIAL为串行队列,这里也可以使用NULL,这个宏定义的值就是NULL,另一种是DISPATCH_QUEUE_CONCURRENT表示并发队列,这里虽然串行队列中的任务是按顺序执行的,但是如果创建多个串行队列,是会并行处理。


    并行执行的多个Serial Dispatch Queue

    由于一个队列只操作一个线程,所以在执行一些比较重要的操作时,尽量使用串行队列,避免造成数据冲突。

    dispatch_(a)sync
    最常用的就是这两个函数了dispatch_sync同步执行,dispatch_async异步执行,第一个参数为指定的队列名,block为执行的任务内容,但是使用gcd也要注意一些问题

        NSLog(@"1");
        
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"2");
        });
        
        NSLog(@"3");
    
     1
    //接下来报错
    
    同步主队列的问题

    看得出这个问题的原因,同步的方式在主线程执行一个任务,造成了线程阻塞。

    dispatch_after
    当我们需要在一段时间之后延迟执行某些任务的时候,可以使用dispatch_after这个函数,示例代码如下:

    NSLog(@"延迟之前");
    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC));
    dispatch_after(time, dispatch_get_main_queue(), ^{
        NSLog(@"延迟3秒执行");
    });
    

    这里的返回结果为

    2018-09-04 14:52:59.437518+0800 GCDDemo[75192:21501573] 延迟之前
    2018-09-04 14:53:02.438042+0800 GCDDemo[75192:21501573] 延迟3秒执行
    

    可以看得出的确是延迟了3秒执行的任务,不过这里的实际意义为在3秒后将任务添加到主队列中执行,由于Main Dispatch Queue在主线程的Runloop中,所以真实的时间为3秒加上最短立刻最长Runloop一次循环的时间,并且如果Runloop中堆积的任务较多,可能时间会更长,所以时间并不是完全精确,不过想要大致完成延迟功能,dispatch_after是完全没有问题的。
    time为dispatch_time_t类型,从第一个参数时间开始,到第二个指定后的时间结束,这里NSEC_PER_SEC是时间类型,以秒为单位,NSEC_PER_MSEC是以毫秒做单位,其他类型单位这里不多解释了。

    dispatch_group
    项目中我们经常会遇到将多个异步任务的结果同时返回,如果只是同步的话,执行完最后一个任务就是结束,但是异步却不行,所以这里我们需要使用dispatch_group,代码如下

        NSLog(@"全部开始-----%@", [NSThread currentThread]);
        dispatch_group_t group = dispatch_group_create();
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        
        dispatch_group_async(group, queue, ^{
            sleep(4);
            NSLog(@"子线程1-----%@", [NSThread currentThread]);
        });
        
        dispatch_group_async(group, queue, ^{
            sleep(3);
            NSLog(@"子线程2-----%@", [NSThread currentThread]);
        });
        
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            NSLog(@"全部结束-----%@", [NSThread currentThread]);
        });
    

    这里使用sleep模拟了两个耗时操作,并且打印了几个位置所处的线程,首先使用dispatch_group_create创建了一个组,并且使用了global queue,接下来模拟异步延时操作,使用dispatch_group_async这个函数和dispatch_async的功能是一样的,只不过前者归属于第一个参数这个组,当函数全部结束之后会调用dispatch_group_notify这个方法,表示所有异步操作都已经结束,代码执行结果如下

    2018-09-04 16:22:03.449143+0800 GCDDemo[76086:21787405] 全部开始-----<NSThread: 0x6040002601c0>{number = 1, name = main}
    2018-09-04 16:22:06.451174+0800 GCDDemo[76086:21787469] 子线程2-----<NSThread: 0x60400027ed40>{number = 3, name = (null)}
    2018-09-04 16:22:07.452564+0800 GCDDemo[76086:21787470] 子线程1-----<NSThread: 0x600000470100>{number = 4, name = (null)}
    2018-09-04 16:22:07.452926+0800 GCDDemo[76086:21787405] 全部结束-----<NSThread: 0x6040002601c0>{number = 1, name = main}
    

    这里可以看出的确是当两个子线程都完成之后,才回到主线程的回调中,另外一种方式通过dispatch_group_wait方式阻止后续操作,直到异步函数全部完成

    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
        NSLog(@"全部结束-----%@", [NSThread currentThread]);
    

    这里wait表示等待,第一个参数表示等待的对象,类型是dispatch_group_t,第二个参数表示等待时间,这里使用DISPATCH_TIME_FOREVER表示一直等待,所以这个函数变成了直到组中代码全部结束,等待才会停止,不过这里还是更加推荐使用dispatch_group_notify方式,因为dispatch_group_wait是同步的,所以不推荐在主线程中使用。

    dispatch_group_enter/dispatch_group_leave
    实际工作中,会出现让多个请求同时返回结果的案例,这时如果用上面的方法会造成一定问题,因为上面异步的任务只是一个NSLog,如果是一个延时请求呢,下面模拟一下多个请求同时发生的案例。

    NSLog(@"全部开始-----%@", [NSThread currentThread]);
        dispatch_group_t group = dispatch_group_create();
        dispatch_queue_t queue = dispatch_queue_create("com.example.gcdDemo", DISPATCH_QUEUE_CONCURRENT);
        
        dispatch_group_async(group, queue, ^{
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                sleep(4);
                NSLog(@"模拟请求1-----%@", [NSThread currentThread]);
            });
        });
        
        dispatch_group_async(group, queue, ^{
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                sleep(3);
                NSLog(@"模拟请求2-----%@", [NSThread currentThread]);
            });
        });
        
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            NSLog(@"全部结束-----%@", [NSThread currentThread]);
        });
    

    这里在dispatch_group_async这些个异步任务中,再次用异步的方式模拟了一个请求任务,下面看一下结果

    2018-09-04 16:50:50.221045+0800 GCDDemo[76421:21817204] 全部开始-----<NSThread: 0x60000007ecc0>{number = 1, name = main}
    2018-09-04 16:50:50.249321+0800 GCDDemo[76421:21817204] 全部结束-----<NSThread: 0x60000007ecc0>{number = 1, name = main}
    2018-09-04 16:50:53.225810+0800 GCDDemo[76421:21817394] 模拟请求2-----<NSThread: 0x604000663000>{number = 5, name = (null)}
    2018-09-04 16:50:54.225917+0800 GCDDemo[76421:21817393] 模拟请求1-----<NSThread: 0x6040004649c0>{number = 3, name = (null)}
    

    造成这样的原因是发起请求的两个任务已经完成了,所以调用了notify方法,但是请求的结果还没有成功,所以这样的代码是有问题的,那么这里就使用dispatch_group_enter和dispatch_group_leave这对方法来解决。

    NSLog(@"全部开始-----%@", [NSThread currentThread]);
        dispatch_group_t group = dispatch_group_create();
        dispatch_queue_t queue = dispatch_queue_create("com.example.gcdDemo", DISPATCH_QUEUE_CONCURRENT);
        
        dispatch_group_async(group, queue, ^{
            dispatch_group_enter(group);
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                sleep(4);
                NSLog(@"模拟请求1-----%@", [NSThread currentThread]);
                dispatch_group_leave(group);
            });
        });
        
        dispatch_group_async(group, queue, ^{
            dispatch_group_enter(group);
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                sleep(3);
                NSLog(@"模拟请求2-----%@", [NSThread currentThread]);
                dispatch_group_leave(group);
            });
        });
        
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            NSLog(@"全部结束-----%@", [NSThread currentThread]);
        });
    

    这样的结果就没有问题了,这对方法可以理解为retain和release,当每次进入到一个新的异步任务中时,使用dispatch_group_enter告诉原本的异步任务这里还没有结束,当请求完成之后使用dispatch_group_leave表示已经结束了,可以退出这个异步操作了,这样的话,才能保证真正完成了一个延时函数,结果如下

    2018-09-04 16:54:22.832901+0800 GCDDemo[76462:21821447] 全部开始-----<NSThread: 0x60000007e980>{number = 1, name = main}
    2018-09-04 16:54:25.837875+0800 GCDDemo[76462:21821502] 模拟请求2-----<NSThread: 0x604000460840>{number = 3, name = (null)}
    2018-09-04 16:54:26.833634+0800 GCDDemo[76462:21821503] 模拟请求1-----<NSThread: 0x6000004676c0>{number = 4, name = (null)}
    2018-09-04 16:54:26.833992+0800 GCDDemo[76462:21821447] 全部结束-----<NSThread: 0x60000007e980>{number = 1, name = main}
    

    当前案例还有其他的解决方案,我准备在下一篇文章中将案例的其他解决办法写出来,这里不加赘述了。

    dispatch_barrier_(a)sync
    再次通过一个案例来认识这个函数,假设有a,b,c,d四个异步任务,我新增加了一个任务new,我希望可以在a和b之后完成并且在c和d之前完成,这里可以通过同步等其他方式解决,不过有个更好的方式解决这个问题,dispatch_barrier_async,和它的名字一样,可以理解为一个栅栏,将前后分隔开,在下先上代码

    dispatch_queue_t queue = dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT);
        dispatch_async(queue, ^{
            sleep(3);
            NSLog(@"task - A");
        });
        dispatch_async(queue, ^{
            sleep(2);
            NSLog(@"task - B");
        });
        
        NSLog(@"before barrier");
        
        dispatch_barrier_sync(queue, ^{
            NSLog(@"task - new");
        });
        
        NSLog(@"after barrier");
        
        dispatch_async(queue, ^{
            sleep(2);
            NSLog(@"task - C");
        });
        
        dispatch_async(queue, ^{
            sleep(1);
            NSLog(@"task - D");
        });
    

    这里模拟了四个异步操作任务,将中间位置插入了一个new任务,这里看看返回结果

    2018-09-04 17:55:08.783989+0800 GCDDemo[76920:21886183] before barrier
    2018-09-04 17:55:10.787647+0800 GCDDemo[76920:21886310] task - B
    2018-09-04 17:55:11.785806+0800 GCDDemo[76920:21886309] task - A
    2018-09-04 17:55:11.786003+0800 GCDDemo[76920:21886183] task - new
    2018-09-04 17:55:11.786164+0800 GCDDemo[76920:21886183] after barrier
    2018-09-04 17:55:12.790452+0800 GCDDemo[76920:21886312] task - D
    2018-09-04 17:55:13.790432+0800 GCDDemo[76920:21886309] task - C
    

    这里可以看出,由于有模拟延时操作,所以before首先输出,然后执行了A和B两个任务,而barrier中的任务并没有延时却在A和B之后,说明栅栏功能生效,有因为是sync同步的关系,所以后面的after紧接着输出,这里把sync换成async会有什么结果呢

    2018-09-04 17:59:29.114406+0800 BlockDemo[76948:21890748] before barrier
    2018-09-04 17:59:29.114600+0800 BlockDemo[76948:21890748] after barrier
    

    before和after是紧挨着输出的,说明dispatch_barrier_sync同样有同步的作用,而dispatch_barrier_async也有着异步的效果。

    dispatch_apply
    如果有一个案例需要按指定次数执行gcd的任务,可以用这个函数,第一个参数为次数,第二个参数为指定的队列,第三个参数是带参数的block,参数为当前次数下标

    NSArray * array = @[@"1",@"2",@"3",@"4",@"5",];
        dispatch_apply([array count], dispatch_get_global_queue(0, 0), ^(size_t index) {
            NSLog(@"元素:%@----第%ld次", array[index],index);
        });
    
    2018-09-04 18:44:56.970194+0800 GCDDemo[77406:21942336] 元素:4----第3次
    2018-09-04 18:44:56.970194+0800 GCDDemo[77406:21942205] 元素:1----第0次
    2018-09-04 18:44:56.970194+0800 GCDDemo[77406:21942337] 元素:3----第2次
    2018-09-04 18:44:56.970194+0800 GCDDemo[77406:21942335] 元素:2----第1次
    2018-09-04 18:44:56.970380+0800 GCDDemo[77406:21942205] 元素:5----第4次
    

    dispatch_semaphore
    前文已经有讲部分关于产生不一样数据的处理问题,不过有时需要更细致的排他控制,例如你到了一个屋子,里面有一个椅子,你可以坐下,如果再拿来一个椅子,依然可以坐下,但是如果把椅子拿走,那么只能站着等待了,semaphore使用信号量的方式实现了类似的情况,首先第一个函数:

    dispatch_semaphore_create(0);
    

    这里创建了一个semaphore,返回类型是dispatch_semaphore_t,有一个参数,表示初始信号量的值,这里给0,如果遇到wait则需要等待。

    dispatch_semaphore_wait(semaphore,DISPATCH_TIME_FOREVER);
    dispatch_semaphore_signal(semaphore);
    

    当第一个函数中的信号量0时,这个函数需要进行等待,如果大于0,则将信号量-1,第二个参数为等待的时间,这个可以根据需求使用,这里假定需要等待无限长的时间,知道信号量增加未知
    第二个函数中会给信号量+1,可以看出这两个函数是要成对出现的,如果单独出现wait而且初始化为0的话,会造成后续任务全部卡住无法执行。下面给一个小的案例

    dispatch_group_t group = dispatch_group_create();
        dispatch_queue_t queue = dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT);
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
        dispatch_group_async(group, queue, ^{
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                sleep(3);
                NSLog(@"完成1");
                dispatch_semaphore_signal(semaphore);
            });
        });
        
        dispatch_group_async(group, queue, ^{
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                sleep(2);
                NSLog(@"完成2");
                dispatch_semaphore_signal(semaphore);
            });
        });
        
        
        dispatch_group_notify(group, queue, ^{
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            NSLog(@"全部完成");
        });
    

    这里再次模拟了多个请求需要同时返回结果的问题,通过信号量的方式,使用信号量的方式同样可以完成这个需求,当notify中有两个等待信号的时候,只能通过请求成功的信号量增加的方法去抵消,当两个请求全部完成的时候,等待信号也全部结束,这时表示任务全部完成。
    不过如果更换一下需求会怎么样呢,如果我们需要让多个请求同步执行要怎么做,首先我们需要开启异步去管理,同样请求也是异步方法,所以我们用这种方式不能让请求同步执行,这里需要使用线程依赖的方式操作,GCD线程依赖这里面也使用semaphore的方式去做,修改代码如下

    dispatch_group_t group = dispatch_group_create();
        dispatch_queue_t queue = dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT);
        dispatch_semaphore_t semaphore0 = dispatch_semaphore_create(0);
        dispatch_semaphore_t semaphore1 = dispatch_semaphore_create(0);
        
        dispatch_group_async(group, queue, ^{
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                [NSThread sleepForTimeInterval:3];
                NSLog(@"完成1");
                dispatch_semaphore_signal(semaphore0);
            });
        });
        
        dispatch_group_async(group, queue, ^{
            dispatch_semaphore_wait(semaphore0,DISPATCH_TIME_FOREVER);
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                [NSThread sleepForTimeInterval:2];
                NSLog(@"完成2");
                dispatch_semaphore_signal(semaphore1);
            });
        });
        
        
        dispatch_group_notify(group, queue, ^{
            dispatch_semaphore_wait(semaphore1, DISPATCH_TIME_FOREVER);
            NSLog(@"全部完成");
        });
    

    这里创建了两个信号量,因为任务1模拟了时间更久,我们这里需要让任务1先完成,那么我们在任务2中把任务1的信号量等待,直到任务1完成并增加信号量,再执行任务2,结果如下

    2018-09-05 10:27:02.725024+0800 GCDDemo[81615:22654482] 完成1
    2018-09-05 10:27:04.727478+0800 GCDDemo[81615:22654482] 完成2
    2018-09-05 10:27:04.727852+0800 GCDDemo[81615:22654485] 全部完成
    

    可以看出我们的目的已经实现了,但是如果有多个请求任务呢,必然需要创建多个信号量,按照需要的顺序进行依赖,但是这种方法其实写起来容易乱,最好的方式是使用NSOperationQueue添加线程依赖,但是这里不多加赘述,同样后续文章中会详细分析一次此案例,并使用其他方式解决这个问题。

    dispatch_once
    这个函数应该也不会陌生,当我们希望只会执行一次的函数我们会使用dispatch_once,所有我们创建单例的时候也会使用到这个函数。

    + (instancetype)shareInstance {
        static Manager * manager = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            manager = [Manager new];
        });
        return manager;
    }
    

    这种单例不需要担心线程问题,即使是多线程环境下也一定是安全的,onceToken会保证运行过程中这部分只会执行一次。

    GCD的实现

    GCD的使用是十分方便的,这里探讨一下它是如何实现的
    C语言实现的用于管理追加Block的FIFO队列
    用于排他控制的atomic类型的轻量级信号
    C语言实现的管理线程的容器
    首先确定一下用于实现dispatch queue的软件组件

    组件和技术
    我们所使用的全部api都处于libdispatch库中,Dispatch Queue通过结构体和链表实现FIFO队列。FIFO通过dispatch_async等函数管理添加的block。
    但是block并不是直接加入到FIFO队列中,而是先加入Dispatch Continuation这个dispatch_continuation_t类型的结构体中,再加入到队列中,这个结构体包含了block所属的group等信息,也就是执行上下文。

    XNU内核有4中workqueue,优先级和Global Dispatch Queue的优先级相同,当在Global Dispatch Queue中执行block时,libdispatch从FIFO队列中取出Dispatch Continuation,调用pthread_workqueue_additem_np函数,将自身信息,符合其优先级的workqueue信息以及执行回调函数等传递给参数。

    pthread_work_queue_additem_np函数使用workq_kernreturn系统调用。通知workqueue应当执行的项目。根据通知,XNU内核基于系统判断是否生成线程,如果是overcommit属性则始终生成线程。

    workqueue的线程执行pthread_workqueue函数,该函数调用libdispatch的回调函数,在回调函数中执行加入到Dispatch Continuation的Block。

    Block执行结束后,进行通知Dispatch Group结束、释放Dispatch Continuation等处理。开始准备执行Global Dispatch Queue的下一个Block

    以上就是Dispatch Queue的大致执行过程。

    Dispatch Source
    GCD中除了常用的Dispatch Queue外,还有Dispatch Source,它具有很多种类型处理能力,最常见的就是使用定时器。

    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
        //创建一个dispatch_source_t类型变量,类型指定为定时器
        dispatch_source_set_timer(timer,DISPATCH_TIME_NOW,1.0*NSEC_PER_SEC, 0);
        //指定定时器执行时间为每秒执行一次
        dispatch_source_set_event_handler(timer, ^{
            NSLog(@"定时内容");
            //每秒执行的内容
        });
        dispatch_source_set_cancel_handler(timer, ^{
            NSLog(@"定时取消");
            //取消定时器的回调
        });
        dispatch_resume(timer);
        //启动定时器
        //dispatch_source_cancel(timer);
        //取消定时器
    

    这里本身会存在一个问题,就是set_event_handle这个回调会不执行,原因是当执行过作用域之后,这个source可能会被释放掉,所以可以使用添加到属性的方式放大source的作用域,保证定时器可以始终执行

    @property (nonatomic, strong) dispatch_source_t timer;
    

    这里如果内存管理语义使用了assign创建定时器,则会报出会被释放的错误。

    使用Dispatch Queue本身是不具备“取消”功能的,要么放弃取消,要么使用NSOperationQueue等方法,而Dispatch Source具备该功能,而且取消后执行的处理可以使用block形式,这里也能看出Dispatch Source的强大功能。

    到此为止关于GCD的这篇文章就结束了,如果文章中出现问题欢迎指出,并且如果有更优秀的看法也欢迎提出或讨论。

    本文部分内容参考自《Objective-C高级编程》一书,有兴趣的小伙伴可以翻看一下

    相关文章

      网友评论

        本文标题:GCD的使用和原理

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