GCD是什么?
Grand Central Dispatch (GCD) comprises language features, runtime libraries, and system enhancements that provide systemic, comprehensive improvements to the support for concurrent code execution on multicore hardware in macOS, iOS, watchOS, and tvOS.
GCD是系统提供的,运用语言特性和运行时,来增强系统在多硬盘情况下对于同时触发的代码执行的处理效率的一种方式。
GCD provides and manages FIFO queues to which your application can submit tasks in the form of block objects.
GCD提供并管理了FIFO队列,你可以在你的应用中通过block对象提交你要执行的代码任务。
摘自here苹果官方文档.
也就是说在开发过程中在需要并发控制时,苹果给我们提供了一种很好的方式:GCD,去帮助我们尽量少的掉进锁和线程的坑里。(关于多线程编程已经在我的另一篇笔记中写过多线程编程此处不在赘述,有兴趣者可自行查看)那么在开发中我们究竟如何使用GCD呢?看官莫急,且听我慢慢讲来
苹果提供的有关GCD的API
1.Dipatch Queue
所谓Dipatch Queue,就是执行处理的等待队列,开发者通过将block对象追加到队列中,队列按照追加的顺序(请注意此处为FIFO)进行对应处理,例如:
dispatch_async(oneQueue, ^{
// 要执行的任务代码
});
需要注意的是,系统给我们提供了两种队列:
Dipatch Queue的种类 | 说明 |
---|---|
Serial Dipatch Queue | 串行队列,队列中的任务会等待现在正在执行中的任务处理结束才会开始执行下一个任务 |
Concurrent Dipatch Queue | 并行队列,队列中的任务不等待现在正在执行的任务处理结束 |
我们可以尝试写一个例子:
dispatch_async(queue, block0);
dispatch_async(queue, block1);
dispatch_async(queue, block2);
dispatch_async(queue, block3);
dispatch_async(queue, block4);
dispatch_async(queue, block5);
dispatch_async(queue, block6);
dispatch_async(queue, block7);
若queue为Serial Dipatch Queue时,因为要等待现在执行中的任务结束,所以输出结果一定是block01234567.
若queue为Concurrent Dipatch Queue时,不必等待当前任务结束,那么输出结果不确定。另外,并行执行,也就是使用多个线程来同时执行多个处理,具体的处理数量由Dipatch Queue中的处理数/cpu内核数量/cpu负荷等当前系统状态来决定。
例如下表所示,他可能在多个线程中执行上述代码的过程是这样的:
线程0 | 线程1 | 线程2 | 线程3 |
---|---|---|---|
block0 | block1 | block2 | block3 |
block5 | block4 | block6 | |
block7 |
也就是目前系统有四个线程并发执行任务,首先block0的任务在线程0中开始执行,然后block1在线程1中/block2在线程2中/block3在线程3中,线程1中的任务最先结束然后开始执行下一个任务block4,之后线程0中的任务结束开始下一个任务block5,然后线程3中的任务结束开始执行下一个任务block6,最后线程0中的任务结束执行下一个任务block7.
有上述例子可知:Concurrent Dipatch Queue中的任务处理顺序会根据处理内容耗时和系统并发处理状态发生改变,所以并不能保证任务的执行顺序,所以对于严格要求执行顺序的操作,请使用Serial Dipatch Queue。
那么我们又应该怎样获取到Dipatch Queue呢?
1.1通过dispatch_queue_create生成Dipatch Queue
我们可以通过调用dispatch_queue_create方法生成
Dipatch Queue,举个:
dispatch_queue_t queue = dispatch_queue_create(parame1:队列名称, parame2:队列类型);
第一个参数即队列名称,用来标识这一队列,我们命名时应遵循简单易懂的原则,另外加入发生崩溃日志中也会打印出来对应队列的名称,所以为了方便调试请慎重取名。
对于第二个参数队列类型,你可以选择指定为NULL,等同与传入DISPATCH_QUEUE_SERIAL,创建得到的队列均为同步队列;传入参数DISPATCH_QUEUE_CONCURRENT则创建所得队列为并行队列。
此处有一个需要注意的点:
-
如果你部署的最低目标低于 iOS 6.0 or Mac OS X 10.8
你应该自己管理GCD对象,使用(dispatch_retain,dispatch_release),ARC并不会去管理它们
-
如果你部署的最低目标是 iOS 6.0 or Mac OS X 10.8 或者更高的
ARC已经能够管理GCD对象了,这时候,GCD对象就如同普通的OC对象一样,不应该使用dispatch_retain ordispatch_release.
因为目前的iOS和系统版本均已升高,所以不在赘述,大家可自行查阅官方文档GCD对象是否需要手动销毁
1.2 获取Main Dispatch Queue/Global Dispatch Queue
在实际开发中,我们并不用特意生成队列,而可以直接使用系统提供给我们的队列,即Main Dispatch Queue(主队列)和Global Dispatch Queue(全局队列)。
主队列是在主线程中执行的,它是一个同步队列。添加到主队列的任务会在主线程的RunLoop中被执行,像一些需要UI更新这种任务我们就可以添加到主队列中。
全局队列是所有应用程序都能使用的并发队列,它有四个优先级:高优先级(DISPATCH_QUEUE_PRIORITY_HIGH)默认优先级(DISPATCH_QUEUE_PRIORITY_DEFAULT)低优先级(DISPATCH_QUEUE_PRIORITY_LOW)和后台优先级
(DISPATCH_QUEUE_PRIORITY_BACKGROUND)
我们可以通过以下代码获取主队列和全局队列:
// 获取主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
// 获取默认优先级全局队列,可以通过修改第一个参数改变其优先级
dispatch_queue_t golbalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
但是需要注意,强烈建议在大多数情况下使用默认优先级的队列就可以了,如果执行的任务需要访问一些共享的资源,那么在不同优先级的队列中调度这些任务很快就会造成不可预期的行为。这样可能会引起程序的完全挂起,因为低优先级的任务始终阻塞了高优先级任务,使它不能被执行。
1.3 dispatch_after
在实际开发过程中,我们常常会遇到这样的需求场景出现:5秒(或者任意指定一段时间)后再执行,这时候就用到了dispatch_after,代码如下:
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 5* NSEC_PER_SEC);
dispatch_async(dispatch_get_main_queue(), ^{
// 处理逻辑
})
第一个参数代表从什么时候开始,一般直接传 DISPATCH_TIME_NOW,表示从现在开始。
第二个参数代表具体多久之后开始,需要注意的是参数类型为int64_t delta,单位是纳秒,为了方便表达,所以可以使用n* NSEC_PER_SEC(秒),当然根据实际需要你也可以选择NSEC_PER_MSEC(毫秒)/ USEC_PER_SEC (微秒)/ NSEC_PER_USEC(纳秒)
另外需要注意的是,dispatch_after其实并不代表着实际多久后执行,而是代表指定时间后将任务追加到队列,所以实际上这并不精准。
1.4 dispatch_group
我们可能会在开发中遇到想要多个任务都处理完成之后执行某项操作,例如下载十张图片,最后显示这十张图片的拼接图片。这时我们有两个选择,第一将这下载的多个任务放进一个同步队列,把拼接显示的最终处理操作放在最后,但是我们都知道下载可能很耗时,那么这种做法可能会造成很大程度上的延时所以并不理想,此时我们还有另外一种方法:使用dispatch_group来实现。dispatch_group正如其名,可以帮我们把一系列操作视为一组操作,他并不关心这组操作的执行顺序是串行还是并行,只关心这一组任务都结束的时间点,这就能很好的帮我们实现上述需求。我们可以编写如下所示的代码:
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, globalQueue, ^{// 下载图片1});
dispatch_group_async(group, globalQueue, ^{// 下载图片2});
dispatch_group_async(group, globalQueue, ^{// 下载图片3});
...
dispatch_group_notify(group, globalQueue, ^{// 拼接显示});
//dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
如代码所示,我们将下载任务添加到了全局队列中异步执行,以group的方式,那么我们就可以通过dispatch_group_notify获取到所有任务结束的节点,从而进行最终的拼接操作。
大家应该注意到了,代码里还有一个函数dispatch_group_wait,这个函数是为了等待加入到group的任务全部执行结束,该函数的第二个参数代表了等待的时间,如果将其设置为DISPATCH_TIME_FOREVER永远,那么只要该gruop中的任务处理还未执行结束,就会一直等待。等待实际上意味着:在指定的时间结束前或者group中的任务执行完成之前,该线程暂停。
1.5 dispatch_barrier_async
顾名思义,这个函数的作用类似于“栅栏”,该函数会等待兵法队列中的操作都执行结束之后,再将指定操作放入并行队列,然后在指定操作执行完之后,才能进行其他任务添加到并行队列开始处理的操作,这个函数在很大程度上帮助我们避免数据和文件访问竞争的问题。
假如我们把读写的操作放入并发队列,那么我们并不能保证写入操作时没有别的线程在读取或写入数据,这就造成了数据竞争。
而使用dispatch_barrier_async函数,我们可以保证单条写入,将写入与读取/其他写入分隔开来,屏蔽数据竞争的问题。
1.6 dispatch_async/dispatch_sync
dispatch_sync意味着同步,也就是将指定任务追加到队列,在追加的任务执行结束之前,dispatch_sync函数会一直等待。
-
dispatch_async意味着异步,将指定任务追加到队列,在追加的任务执行结束之前,dispatch_async函数不会做任何等待。
所以如果我们在一个任务处理结束后要使用到该任务的结果,那么我们就需要使用dispatch_sync等待执行结果,这很方便。但是有一点,容易引起死锁。举个:dispatch_queue_t queue = dispatch_get_main_queue(); dispatch_sync(queue, ^{// 某个任务});
上述代码将一个任务使用同步方式放进了主队列,并且等待执行的结束,但是实际上主队列就正在执行这些代码,所以无法执行追加的这一任务,这就造成了相互等待的死锁。所以dispatch_sync需要谨慎使用。
1.7 dispatch_suspend/dispatch_resume
dispatch_suspend函数作用为挂起指定队列,dispatch_resume则恢复指定队列的执行
dispatch_suspend(queue);
dispatch_resume(queue);
这两个函数对已经执行的任务没有影响,而在挂起后在队列中尚未被执行的任务在挂起后停止执行,在恢复后继续执行。
1.8 Dispatch Semaphore
Dispatch Semaphore指的是信号量,当其信号量指示为0时等待,信号量指示为1或大于1时,减去1而并不等待。这为我们很好的实现了较细粒度的排他控制。
例如假设现在系统有两个空闲资源可以被利用,但同一时间却有三个线程要进行访问,这种时候我们就可以使用这种类似锁的方式Dispatch Semaphore来控制。
//创建信号量,参数:信号量的初值,如果小于0则会返回NULL
dispatch_semaphore_create(信号量值)
//降低信号量,等待任务结束
dispatch_semaphore_wait(信号量,等待时间)
//资源被释放,提高信号量
dispatch_semaphore_signal(信号量)
对于我们提出的三个任务竞争两个资源的问题,解决代码如下:
//首先创建对应资源数量的信号量
dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
dispatch_queue_t quene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//添加任务1
dispatch_async(quene, ^{
// 任务一占用一个资源,信号量-1
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run task 1");
sleep(1);
NSLog(@"complete task 1");
// 任务一完成,资源可回收,信号量+1
dispatch_semaphore_signal(semaphore);
});
//任务2处理流程与任务1相同
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run task 2");
sleep(1);
NSLog(@"complete task 2");
dispatch_semaphore_signal(semaphore);
});
//任务3处理流程与任务1相同
dispatch_async(quene, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run task 3");
sleep(1);
NSLog(@"complete task 3");
dispatch_semaphore_signal(semaphore);
});
1.9 dispatch_once
dispatch_once保证了应用程序只执行一次指定任务,保证了多线程环境下也是安全的,例如我们肯定在单例中频繁接触到这一函数。
参考书:【Objective-C高级编程 iOS与OS X多线程和内存管理】